def postFormWithParts( self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Optional[Callable[[int, int], None]] = None) -> QNetworkReply: self._validateManager() request = self._createEmptyRequest(target, content_type=None) multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType) for part in parts: multi_post_part.append(part) self._last_request_time = time() if self._manager is not None: reply = self._manager.post(request, multi_post_part) self._kept_alive_multiparts[reply] = multi_post_part if on_progress is not None: reply.uploadProgress.connect(on_progress) self._registerOnFinishedCallback(reply, on_finished) return reply else: Logger.log("e", "Could not find manager.")
def sendAttachments(self, featureid, files): multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) for fileName, fileData in files.items(): file_part = QHttpPart() file_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="filedata"; filename="%s"' % fileName) file_part.setHeader(QNetworkRequest.ContentTypeHeader, "application/octet-stream") file_part.setBody(fileData) multi_part.append(file_part) content = self.sendRequest( '/upload/%s' % featureid, {'token':self.token}, 'post', multi_part ) return json.loads(content)
def sendGeoJSON(self, data, filename, projectid, data_format): multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) format_part = QHttpPart() format_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="format"') format_part.setBody(str(data_format).encode('utf-8')) file_part = QHttpPart() file_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="file[0]"; filename="%s.sqlite"' % filename) file_part.setHeader(QNetworkRequest.ContentTypeHeader, "application/octet-stream") file_part.setBody(data) multi_part.append(format_part) multi_part.append(file_part) content = self.sendRequest( '/upload_gis/%s/new' % projectid, {'token':self.token}, 'post', multi_part ) return json.loads(content)
def postFormWithParts(self, target:str, parts: List[QHttpPart], onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target, content_type=None) multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType) for part in parts: multi_post_part.append(part) self._last_request_time = time() reply = self._manager.post(request, multi_post_part) self._kept_alive_multiparts[reply] = multi_post_part if onProgress is not None: reply.uploadProgress.connect(onProgress) self._registerOnFinishedCallback(reply, onFinished) return reply
def generate_multipart_data(text_dict=None, file_dict=None): multipart_data = QHttpMultiPart(QHttpMultiPart.FormDataType) if text_dict: for key, value in text_dict.items(): text_part = QHttpPart() text_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data;name=\"%s\"" % key) text_part.setBody(str(value).encode("utf-8")) multipart_data.append(text_part) if file_dict: for key, file in file_dict.items(): file_part = QHttpPart() filename = QFileInfo(file.fileName()).fileName() file_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"%s\"; filename=\"%s\"" % (key, filename)) file_part.setBodyDevice(file) file.setParent(multipart_data) multipart_data.append(file_part) return multipart_data
def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Optional[Callable[[int, int], None]] = None) -> QNetworkReply: self._validateManager() request = self._createEmptyRequest(target, content_type=None) multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType) for part in parts: multi_post_part.append(part) self._last_request_time = time() if self._manager is not None: reply = self._manager.post(request, multi_post_part) self._kept_alive_multiparts[reply] = multi_post_part if on_progress is not None: reply.uploadProgress.connect(on_progress) self._registerOnFinishedCallback(reply, on_finished) return reply else: Logger.log("e", "Could not find manager.")
def sendRaster(self, data, filename, projectid, crs): multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) name_part = QHttpPart() name_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="name"') name_part.setBody(filename.encode('utf-8')) crs_part = QHttpPart() crs_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="srs"') crs_part.setBody(str(crs).encode('utf-8')) file_part = QHttpPart() file_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="file[0]"; filename="%s.tiff"' % filename) file_part.setHeader(QNetworkRequest.ContentTypeHeader, "application/octet-stream") file_part.setBody(data) multi_part.append(name_part) multi_part.append(crs_part) multi_part.append(file_part) content = self.sendRequest( '/rasters/%s' % projectid, {'token':self.token}, 'post', multi_part ) if content: return json.loads(content)
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_component: 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 = QUrl.fromLocalFile(os.path.join(self._plugin_path, "PrintWindow.qml")) component = QQmlComponent(Application.getInstance()._engine, path) self._print_context = QQmlContext(Application.getInstance()._engine.rootContext()) self._print_context.setContextProperty("OutputDevice", self) self._print_view = component.create(self._print_context) if component.isError(): Logger.log("e", " Errors creating component: \n%s", "\n".join( [e.toString() for e in component.errors()])) 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 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 <filename>{file_name}</filename> 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().showPrintMonitor.emit(False) @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())
class OctoPrintOutputDevice(PrinterOutputDevice): def __init__(self, key, address, port, properties): super().__init__(key) self._address = address self._port = port self._path = properties.get(b"path", b"/").decode("utf-8") if self._path[-1:] != "/": self._path += "/" self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None self._auto_print = True ## We start with a single extruder, but update this when we get data from octoprint self._num_extruders_set = False self._num_extruders = 1 self._api_version = "1" self._api_prefix = "api/" self._api_header = "X-Api-Key" self._api_key = None protocol = "https" if properties.get(b'useHttps') == b"true" else "http" self._base_url = "%s://%s:%d%s" % (protocol, self._address, self._port, self._path) self._api_url = self._base_url + self._api_prefix self._camera_url = "%s://%s:8080/?action=stream" % (protocol, self._address) self.setPriority(2) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with OctoPrint")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint")) self.setIconName("print") self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None self._printer_reply = None self._print_job_request = None self._print_job_reply = None self._image_request = None self._image_reply = None self._stream_buffer = b"" self._stream_buffer_start_index = -1 self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None self._job_request = None self._job_reply = None self._command_request = None self._command_reply = 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_image_id = 0 self._camera_image = QImage() self._connection_state_before_timeout = None self._last_response_time = None self._last_request_time = None self._response_timeout_time = 5 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._preheat_timer = QTimer() self._preheat_timer.setSingleShot(True) self._preheat_timer.timeout.connect(self.cancelPreheatBed) 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 ## Set the API key of this OctoPrint instance def setApiKey(self, api_key): self._api_key = api_key ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant = True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self): return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self): return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self): return self._base_url def _startCamera(self): global_container_stack = Application.getInstance().getGlobalContainerStack() if not global_container_stack or not parseBool(global_container_stack.getMetaDataEntry("octoprint_show_camera", False)): return # Start streaming mjpg stream url = QUrl(self._camera_url) self._image_request = QNetworkRequest(url) self._image_reply = self._manager.get(self._image_request) self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress) def _stopCamera(self): if self._image_reply: self._image_reply.abort() self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress) self._image_reply = None self._image_request = None self._stream_buffer = b"" self._stream_buffer_start_index = -1 self._camera_image = QImage() self.newImage.emit() 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 # 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: self._recreate_network_manager_count += 1 Logger.log("d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", 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.")) self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: Logger.log("d", "Stopping post upload because the connection was lost.") try: self._post_reply.uploadProgress.disconnect(self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. 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 %s seconds, so it seems OctoPrint is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with OctoPrint was lost. Check your network-connections.")) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data url = QUrl(self._api_url + "printer") self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) ## Request print_job data url = QUrl(self._api_url + "job") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_reply = self._manager.get(self._job_request) def _createNetworkManager(self): if self._manager: self._manager.finished.disconnect(self._onRequestFinished) self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) def close(self): self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() self._stopCamera() def requestWrite(self, node, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): self.writeStarted.emit(self) self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from the instance def connect(self): self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. Logger.log("d", "Connection with instance %s with url %s started", self._key, self._base_url) self._update_timer.start() self._last_response_time = None self.setAcceptsCommands(False) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connecting to OctoPrint on {0}").format(self._key)) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %s with url %s stopped", self._key, self._base_url) 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): if job_state == "abort": command = "cancel" elif job_state == "print": if self.jobState == "paused": command = "pause" else: command = "start" elif job_state == "pause": command = "pause" if command: self._sendJobCommand(command) def startPrint(self): global_container_stack = Application.getInstance().getGlobalContainerStack() if not global_container_stack: return if self.jobState not in ["ready", ""]: if self.jobState == "offline": self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is offline. Unable to start a new job.")) else: self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is busy. Unable to start a new job.")) self._error_message.show() return self._preheat_timer.stop() self._auto_print = parseBool(global_container_stack.getMetaDataEntry("octoprint_auto_print", True)) if self._auto_print: Application.getInstance().showPrintMonitor.emit(True) try: self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1) self._progress_message.addAction("Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect(self._cancelSendGcode) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" last_process_events = time() for line in self._gcode: single_string_file_data += line if time() > last_process_events + 0.05: # Ensure that the GUI keeps updated at least 20 times per second. QCoreApplication.processEvents() last_process_events = time() job_name = Application.getInstance().getPrintInformation().jobName.strip() if job_name is "": job_name = "untitled_print" file_name = "%s.gcode" % job_name ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) if self._auto_print: self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) destination = "local" if parseBool(global_container_stack.getMetaDataEntry("octoprint_store_sd", False)): destination = "sdcard" url = QUrl(self._api_url + "files/" + destination) ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log("e", "An exception occurred in network connection: %s" % str(e)) def _cancelSendGcode(self, message_id, action_id): if self._post_reply: Logger.log("d", "Stopping upload because the user pressed cancel.") try: self._post_reply.uploadProgress.disconnect(self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._post_reply = None self._progress_message.hide() def _sendCommand(self, command): self._sendCommandToApi("printer/command", command) Logger.log("d", "Sent gcode command to OctoPrint instance: %s", command) def _sendJobCommand(self, command): self._sendCommandToApi("job", command) Logger.log("d", "Sent job command to OctoPrint instance: %s", command) def _sendCommandToApi(self, endpoint, command): url = QUrl(self._api_url + endpoint) self._command_request = QNetworkRequest(url) self._command_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = "{\"command\": \"%s\"}" % command self._command_reply = self._manager.post(self._command_request, data.encode()) ## 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): self._setTargetBedTemperature(temperature) if duration > 0: self._preheat_timer.setInterval(duration * 1000) self._preheat_timer.start() else: self._preheat_timer.stop() ## Cancels pre-heating the heated bed of the printer. # # If the bed is not pre-heated, nothing happens. @pyqtSlot() def cancelPreheatBed(self): self._setTargetBedTemperature(0) self._preheat_timer.stop() def _setTargetBedTemperature(self, temperature): Logger.log("d", "Setting bed temperature to %s", temperature) self._sendCommand("M140 S%s" % temperature) def _setTargetHotendTemperature(self, index, temperature): Logger.log("d", "Setting hotend %s temperature to %s", index, temperature) self._sendCommand("M104 T%s S%s" % (index, temperature)) def _setHeadPosition(self, x, y , z, speed): self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) def _setHeadX(self, x, speed): self._sendCommand("G0 X%s F%s" % (x, speed)) def _setHeadY(self, y, speed): self._sendCommand("G0 Y%s F%s" % (y, speed)) def _setHeadZ(self, z, speed): self._sendCommand("G0 Y%s F%s" % (z, speed)) def _homeHead(self): self._sendCommand("G28") def _homeBed(self): self._sendCommand("G28 Z") def _moveHead(self, x, y, z, speed): self._sendCommand("G91") self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) self._sendCommand("G90") ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state 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. if self._last_response_time: Logger.log("d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return if reply.operation() == QNetworkAccessManager.GetOperation: if self._api_prefix + "printer" in reply.url().toString(): # Status update from /printer. if http_status_code == 200: if not self.acceptsCommands: self.setAcceptsCommands(True) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key)) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) if "temperature" in json_data: if not self._num_extruders_set: self._num_extruders = 0 while "tool%d" % self._num_extruders in json_data["temperature"]: self._num_extruders = self._num_extruders + 1 # Reinitialise from PrinterOutputDevice to match the new _num_extruders self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders self._num_extruders_set = True # Check for hotend temperatures for index in range(0, self._num_extruders): temperature = json_data["temperature"]["tool%d" % index]["actual"] if ("tool%d" % index) in json_data["temperature"] else 0 self._setHotendTemperature(index, temperature) bed_temperature = json_data["temperature"]["bed"]["actual"] if "bed" in json_data["temperature"] else 0 self._setBedTemperature(bed_temperature) job_state = "offline" if "state" in json_data: if json_data["state"]["flags"]["error"]: job_state = "error" elif json_data["state"]["flags"]["paused"]: job_state = "paused" elif json_data["state"]["flags"]["printing"]: job_state = "printing" elif json_data["state"]["flags"]["ready"]: job_state = "ready" self._updateJobState(job_state) elif http_status_code == 401: self._updateJobState("offline") self.setConnectionText(i18n_catalog.i18nc("@info:status", "OctoPrint on {0} does not allow access to print").format(self._key)) elif http_status_code == 409: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) self._updateJobState("offline") self.setConnectionText(i18n_catalog.i18nc("@info:status", "The printer connected to OctoPrint on {0} is not operational").format(self._key)) else: self._updateJobState("offline") Logger.log("w", "Received an unexpected returncode: %d", http_status_code) elif self._api_prefix + "job" in reply.url().toString(): # Status update from /job: if http_status_code == 200: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) progress = json_data["progress"]["completion"] if progress: self.setProgress(progress) if json_data["progress"]["printTime"]: self.setTimeElapsed(json_data["progress"]["printTime"]) if json_data["progress"]["printTimeLeft"]: self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) elif json_data["job"]["estimatedPrintTime"]: self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"])) elif progress > 0: self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100)) else: self.setTimeTotal(0) else: self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName(json_data["job"]["file"]["name"]) else: pass # TODO: Handle errors elif reply.operation() == QNetworkAccessManager.PostOperation: if self._api_prefix + "files" in reply.url().toString(): # Result from /files command: if http_status_code == 201: Logger.log("d", "Resource created on OctoPrint instance: %s", reply.header(QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() global_container_stack = Application.getInstance().getGlobalContainerStack() if not self._auto_print: location = reply.header(QNetworkRequest.LocationHeader) if location: file_name = QUrl(reply.header(QNetworkRequest.LocationHeader).toString()).fileName() message = Message(i18n_catalog.i18nc("@info:status", "Saved to OctoPrint as {0}").format(file_name)) else: message = Message(i18n_catalog.i18nc("@info:status", "Saved to OctoPrint")) message.addAction("open_browser", i18n_catalog.i18nc("@action:button", "Open OctoPrint..."), "globe", i18n_catalog.i18nc("@info:tooltip", "Open the OctoPrint web interface")) message.actionTriggered.connect(self._onMessageActionTriggered) message.show() elif self._api_prefix + "job" in reply.url().toString(): # Result from /job command: if http_status_code == 204: Logger.log("d", "Octoprint command accepted") else: pass # TODO: Handle errors else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) def _onStreamDownloadProgress(self, bytes_received, bytes_total): self._stream_buffer += self._image_reply.readAll() 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 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: # 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() progress = bytes_sent / bytes_total * 100 if progress < 100: if progress > self._progress_message.getProgress(): self._progress_message.setProgress(progress) else: self._progress_message.hide() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Storing data on OctoPrint"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self._base_url))
class OctoPrintOutputDevice(PrinterOutputDevice): def __init__(self, key, address, properties): super().__init__(key) self._address = address self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None ## Todo: Hardcoded value now; we should probably read this from the machine definition and octoprint. self._num_extruders = 1 self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders self._api_version = "1" self._api_prefix = "/api/" self._api_header = "X-Api-Key" self._api_key = None self.setName(key) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print with OctoPrint")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint")) self.setIconName("print") # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None self._printer_reply = None self._print_job_request = None self._print_job_reply = None self._image_request = None self._image_reply = None self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None self._job_request = None self._job_reply = None self._progress_message = None self._error_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._update_camera) self._camera_image_id = 0 self._camera_image = QImage() def getProperties(self): return self._properties ## 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 ## Set the API key of this OctoPrint instance def setApiKey(self, api_key): self._api_key = api_key ## Name of the printer (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this printer @pyqtProperty(str, constant=True) def ipAddress(self): return self._address def _update_camera(self): ## Request new image url = QUrl("http://" + self._address + ":8080/?action=snapshot") self._image_request = QNetworkRequest(url) self._image_reply = self._manager.get(self._image_request) def _update(self): ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) ## Request print_job data url = QUrl("http://" + self._address + self._api_prefix + "job") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_reply = self._manager.get(self._job_request) def close(self): self.setConnectionState(ConnectionState.closed) self._update_timer.stop() self._camera_timer.stop() def requestWrite(self, node, file_name=None, filter_by_machine=False): self._gcode = getattr( Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from printer def connect(self): self.setConnectionState(ConnectionState.connecting) self._update( ) # Manually trigger the first update, as we don't want to wait a few secs before it starts. self._update_camera() Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address) self._update_timer.start() self._camera_timer.start() newImage = pyqtSignal() @pyqtProperty(QUrl, notify=newImage) def cameraImage(self): self._camera_image_id += 1 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): url = QUrl("http://" + self._address + self._api_prefix + "job") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") if job_state == "abort": command = "cancel" elif job_state == "print": if self.jobState == "paused": command = "pause" else: command = "start" elif job_state == "pause": command = "pause" data = "{\"command\": \"%s\"}" % command self._job_reply = self._manager.post(self._job_request, data.encode()) Logger.log("d", "Sent command to OctoPrint instance: %s", data) def startPrint(self): if self.jobState != "ready" and self.jobState != "": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Printer is printing. Unable to start a new job.")) self._error_message.show() return try: self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" for line in self._gcode: single_string_file_data += line ## TODO: Use correct file name (we use placeholder now) file_name = "%s.gcode" % Application.getInstance( ).getPrintInformation().jobName ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) url = QUrl("http://" + self._address + self._api_prefix + "files/local") ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None 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?" )) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) ## Handler for all requests that have finished. def _onFinished(self, reply): http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString( ): # Status update from /printer. if http_status_code == 200: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) # Check for hotend temperatures for index in range(0, self._num_extruders): temperature = json_data["temperature"]["tool%d" % index]["actual"] self._setHotendTemperature(index, temperature) bed_temperature = json_data["temperature"]["bed"]["actual"] self._setBedTemperature(bed_temperature) printer_state = "offline" if json_data["state"]["flags"]["error"]: printer_state = "error" elif json_data["state"]["flags"]["paused"]: printer_state = "paused" elif json_data["state"]["flags"]["printing"]: printer_state = "printing" elif json_data["state"]["flags"]["ready"]: printer_state = "ready" self._updateJobState(printer_state) else: pass # TODO: Handle errors elif "job" in reply.url().toString(): # Status update from /job: if http_status_code == 200: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) progress = json_data["progress"]["completion"] if progress: self.setProgress(progress) if json_data["progress"]["printTime"]: self.setTimeElapsed(json_data["progress"]["printTime"]) if json_data["progress"]["printTimeLeft"]: self.setTimeTotal( json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) elif json_data["job"]["estimatedPrintTime"]: self.setTimeTotal( json_data["job"]["estimatedPrintTime"]) elif progress > 0: self.setTimeTotal( json_data["progress"]["printTime"] / (progress / 100)) else: self.setTimeTotal(0) else: self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName(json_data["job"]["file"]["name"]) else: pass # TODO: Handle errors elif "snapshot" in reply.url().toString(): # Update from camera: if reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) == 200: self._camera_image.loadFromData(reply.readAll()) self.newImage.emit() else: pass # TODO: Handle errors elif reply.operation() == QNetworkAccessManager.PostOperation: if "files" in reply.url().toString( ): # Result from /files command: if http_status_code == 201: Logger.log( "d", "Resource created on OctoPrint instance: %s", reply.header( QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() elif "job" in reply.url().toString(): # Result from /job command: if http_status_code == 204: Logger.log("d", "Octoprint command accepted") else: pass # TODO: Handle errors else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: self._progress_message.setProgress(0)
class MKSOutputDevice(NetworkedPrinterOutputDevice): def __init__(self, instance_id: str, address: str, properties: dict, **kwargs) -> None: super().__init__(device_id=instance_id, address=address, properties=properties, **kwargs) self._address = address self._port = 8080 self._key = instance_id self._properties = properties self._target_bed_temperature = 0 self._num_extruders = 1 self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "MonitorItem4x.qml") # self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") self.setPriority( 3 ) # Make sure the output device gets selected above local file output and Octoprint XD self._active_machine = CuraApplication.getInstance().getMachineManager( ).activeMachine self.setName(instance_id) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print over TFT")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print over TFT")) self.setIconName("print") self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected to TFT on {0}").format(self._key)) Application.getInstance().globalContainerStackChanged.connect( self._onGlobalContainerChanged) self._socket = None self._gl = None self._command_queue = Queue() self._isPrinting = False self._isPause = False self._isSending = False self._gcode = None self._isConnect = False self._printing_filename = "" self._printing_progress = 0 self._printing_time = 0 self._start_time = 0 self._pause_time = 0 self.last_update_time = 0 self.angle = 10 self._connection_state_before_timeout = None self._sdFileList = False self.sdFiles = [] self._mdialog = None self._mfilename = None self._uploadpath = '' self._settings_reply = None self._printer_reply = None self._job_reply = None self._command_reply = None self._screenShot = None self._image_reply = None self._stream_buffer = b"" self._stream_buffer_start_index = -1 self._post_reply = None self._post_multi_part = None self._post_part = None self._last_file_name = None self._last_file_path = None self._progress_message = None self._error_message = None self._connection_message = None self.__additional_components_view = 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._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) self._preheat_timer = QTimer() self._preheat_timer.setSingleShot(True) self._preheat_timer.timeout.connect(self.cancelPreheatBed) self._exception_message = None self._output_controller = GenericOutputController(self) self._number_of_extruders = 1 self._camera_url = "" # Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) CuraApplication.getInstance().getCuraSceneController( ).activeBuildPlateChanged.connect(self.CreateMKSController) def _onOutputDevicesChanged(self): Logger.log("d", "MKS _onOutputDevicesChanged") def connect(self): if self._socket is not None: self._socket.close() self._socket = QTcpSocket() self._socket.connectToHost(self._address, self._port) global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() self.setShortDescription( i18n_catalog.i18nc( "@action:button", "Print over " + global_container_stack.getName())) self.setDescription( i18n_catalog.i18nc( "@properties:tooltip", "Print over " + global_container_stack.getName())) Logger.log("d", "MKS socket connecting ") # self._socket.waitForConnected(2000) self.setConnectionState( cast(ConnectionState, UnifiedConnectionState.Connecting)) self._setAcceptsCommands(True) self._socket.readyRead.connect(self.on_read) self._update_timer.start() 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 "" @pyqtSlot(result=str) def getKey(self): return self._key @pyqtProperty(str, constant=True) def address(self): return self._properties.get(b"address", b"").decode("utf-8") @pyqtProperty(str, constant=True) def name(self): return self._properties.get(b"name", b"").decode("utf-8") @pyqtProperty(str, constant=True) def firmwareVersion(self): return self._properties.get(b"firmware_version", b"").decode("utf-8") @pyqtProperty(str, constant=True) def ipAddress(self): return self._address @pyqtSlot(float, float) def preheatBed(self, temperature, duration): self._setTargetBedTemperature(temperature) if duration > 0: self._preheat_timer.setInterval(duration * 1000) self._preheat_timer.start() else: self._preheat_timer.stop() @pyqtSlot() def cancelPreheatBed(self): self._setTargetBedTemperature(0) self._preheat_timer.stop() @pyqtSlot() def printtest(self): self.sendCommand("M104 S0\r\n M140 S0\r\n M106 S255") @pyqtSlot() def printer_state(self): if len(self._printers) <= 0: return "offline" return self.printers[0].state @pyqtSlot() def selectfile(self): if self._last_file_name: return True else: return False @pyqtSlot(str) def deleteSDFiles(self, filename): # filename = "几何图.gcode" self._sendCommand("M30 1:/" + filename) self.sdFiles.remove(filename) self._sendCommand("M20") @pyqtSlot(str) def printSDFiles(self, filename): self._sendCommand("M23 " + filename) self._sendCommand("M24") @pyqtSlot() def selectFileToUplload(self): preferences = Application.getInstance().getPreferences() preferences.addPreference("mkswifi/autoprint", "True") preferences.addPreference("mkswifi/savepath", "") filename, _ = QFileDialog.getOpenFileName( None, "choose file", preferences.getValue("mkswifi/savepath"), "Gcode(*.gcode;*.g;*.goc)") preferences.setValue("mkswifi/savepath", filename) self._uploadpath = filename if ".g" in filename.lower(): # Logger.log("d", "selectfile:"+filename) if filename in self.sdFiles: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle("The " + filename[filename.rfind("/") + 1:] + " file already exists.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(lambda: self.renameupload(filename)) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "The " + filename[filename.rfind("/") + 1:] + " file already exists. Do you want to rename and upload it?" ) self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if len(filename[filename.rfind("/") + 1:]) >= 30: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle( "File name is too long to upload, please rename it.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(lambda: self.renameupload(filename)) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "File name is too long to upload, please rename it.") self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if self.isBusy(): if self._exception_message: self._exception_message.hide() self._exception_message = Message( i18n_catalog.i18nc( "@info:status", "File cannot be transferred during printing.")) self._exception_message.show() return self.uploadfunc(filename) def closeMDialog(self): if self._mdialog: self._mdialog.close() def renameupload(self, filename): if self._mfilename and ".g" in self._mfilename.text().lower(): filename = filename[:filename. rfind("/")] + "/" + self._mfilename.text() if self._mfilename.text() in self.sdFiles: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle("The " + filename[filename.rfind("/") + 1:] + " file already exists.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(lambda: self.renameupload(filename)) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "The " + filename[filename.rfind("/") + 1:] + " file already exists. Do you want to rename and upload it?" ) self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if len(filename[filename.rfind("/") + 1:]) >= 30: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle( "File name is too long to upload, please rename it.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(lambda: self.renameupload(filename)) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "File name is too long to upload, please rename it.") self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if self.isBusy(): if self._exception_message: self._exception_message.hide() self._exception_message = Message( i18n_catalog.i18nc( "@info:status", "File cannot be transferred during printing.")) self._exception_message.show() return self._mdialog.close() self.uploadfunc(filename) def uploadfunc(self, filename): preferences = Application.getInstance().getPreferences() preferences.addPreference("mkswifi/autoprint", "True") preferences.addPreference("mkswifi/savepath", "") self._update_timer.stop() self._isSending = True self._preheat_timer.stop() single_string_file_data = "" try: f = open(self._uploadpath, "r") single_string_file_data = f.read() file_name = filename[filename.rfind("/") + 1:] self._last_file_name = file_name self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data"), option_text=i18n_catalog.i18nc("@label", "Print jobs"), option_state=preferences.getValue("mkswifi/autoprint")) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._cancelSendGcode) self._progress_message.optionToggled.connect( self._onOptionStateChanged) self._progress_message.show() self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) self._post_part = QHttpPart() self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) post_request = QNetworkRequest( QUrl("http://%s/upload?X-Filename=%s" % (self._address, file_name))) post_request.setRawHeader(b'Content-Type', b'application/octet-stream') post_request.setRawHeader(b'Connection', b'keep-alive') self._post_reply = self._manager.post(post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._post_reply.sslErrors.connect(self._onUploadError) self._gcode = None except IOError as e: Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Send file to printer failed.")) self._error_message.show() self._update_timer.start() except Exception as e: self._update_timer.start() self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) @pyqtProperty("QVariantList") def getSDFiles(self): self._sendCommand("M20") return list(self.sdFiles) def _setTargetBedTemperature(self, temperature): if not self._updateTargetBedTemperature(temperature): return self._sendCommand(["M140 S%s" % temperature]) @pyqtSlot(str) def sendCommand(self, cmd): self._sendCommand(cmd) def _sendCommand(self, cmd): # Logger.log("d", "_sendCommand %s" % str(cmd)) if self._socket and self._socket.state() == 2 or self._socket.state( ) == 3: if isinstance(cmd, str): self._command_queue.put(cmd + "\r\n") elif isinstance(cmd, list): for eachCommand in cmd: self._command_queue.put(eachCommand + "\r\n") def disconnect(self): # self._updateJobState("") self._isConnect = False self.setConnectionState( cast(ConnectionState, UnifiedConnectionState.Closed)) if self._socket is not None: self._socket.readyRead.disconnect(self.on_read) self._socket.close() if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() def isConnected(self): return self._isConnect def isBusy(self): return self._isPrinting or self._isPause def requestWrite(self, node, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self.writeStarted.emit(self) self._update_timer.stop() self._isSending = True # imagebuff = self._gl.glReadPixels(0, 0, 800, 800, self._gl.GL_RGB, # self._gl.GL_UNSIGNED_BYTE) active_build_plate = CuraApplication.getInstance( ).getMultiBuildPlateModel().activeBuildPlate scene = CuraApplication.getInstance().getController().getScene() gcode_dict = getattr(scene, "gcode_dict", None) if not gcode_dict: return self._gcode = gcode_dict.get(active_build_plate, None) # Logger.log("d", "mks ready for print") self.startPrint() def startPrint(self): global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() if not global_container_stack: return if self._error_message: self._error_message.hide() self._error_message = None if self._progress_message: self._progress_message.hide() self._progress_message = None if self.isBusy(): self._error_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data")) self._error_message.show() return job_name = Application.getInstance().getPrintInformation( ).jobName.strip() if job_name is "": job_name = "untitled_print" job_name = "cura_file" filename = "%s.gcode" % job_name if filename in self.sdFiles: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle("The " + filename[filename.rfind("/") + 1:] + " file already exists.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(self.recheckfilename) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "The " + filename[filename.rfind("/") + 1:] + " file already exists. Do you want to rename and upload it?") self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if len(filename[filename.rfind("/") + 1:]) >= 30: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle( "File name is too long to upload, please rename it.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(self.recheckfilename) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "File name is too long to upload, please rename it.") self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return self._startPrint(filename) def recheckfilename(self): if self._mfilename and ".g" in self._mfilename.text().lower(): filename = self._mfilename.text() if filename in self.sdFiles: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle("The " + filename[filename.rfind("/") + 1:] + " file already exists.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(self.recheckfilename) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "The " + filename[filename.rfind("/") + 1:] + " file already exists. Do you want to rename and upload it?" ) self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if len(filename[filename.rfind("/") + 1:]) >= 30: if self._mdialog: self._mdialog.close() self._mdialog = QDialog() self._mdialog.setWindowTitle( "File name is too long to upload, please rename it.") dialogvbox = QVBoxLayout() dialoghbox = QHBoxLayout() yesbtn = QPushButton("yes") nobtn = QPushButton("no") yesbtn.clicked.connect(self.recheckfilename) nobtn.clicked.connect(self.closeMDialog) content = QLabel( "File name is too long to upload, please rename it.") self._mfilename = QLineEdit() self._mfilename.setText(filename[filename.rfind("/") + 1:]) dialoghbox.addWidget(yesbtn) dialoghbox.addWidget(nobtn) dialogvbox.addWidget(content) dialogvbox.addWidget(self._mfilename) dialogvbox.addLayout(dialoghbox) self._mdialog.setLayout(dialogvbox) self._mdialog.exec_() return if self.isBusy(): if self._exception_message: self._exception_message.hide() self._exception_message = Message( i18n_catalog.i18nc( "@info:status", "File cannot be transferred during printing.")) self._exception_message.show() return self._mdialog.close() self._startPrint(filename) def _messageBoxCallback(self, button): def delayedCallback(): if button == QMessageBox.Yes: self.startPrint() else: CuraApplication.getInstance().getController().setActiveStage( "PrepareStage") def _startPrint(self, file_name="cura_file.gcode"): self._preheat_timer.stop() self._screenShot = utils.take_screenshot() try: preferences = Application.getInstance().getPreferences() preferences.addPreference("mkswifi/autoprint", "True") preferences.addPreference("mkswifi/savepath", "") # CuraApplication.getInstance().showPrintMonitor.emit(True) self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data"), option_text=i18n_catalog.i18nc("@label", "Print jobs"), option_state=preferences.getValue("mkswifi/autoprint")) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._cancelSendGcode) self._progress_message.optionToggled.connect( self._onOptionStateChanged) self._progress_message.show() # job_name = Application.getInstance().getPrintInformation().jobName.strip() # if job_name is "": # job_name = "untitled_print" # job_name = "cura_file" # file_name = "%s.gcode" % job_name self._last_file_name = file_name Logger.log( "d", "mks: " + file_name + Application.getInstance(). getPrintInformation().jobName.strip()) single_string_file_data = "" if self._screenShot: single_string_file_data += utils.add_screenshot( self._screenShot, 50, 50, ";simage:") single_string_file_data += utils.add_screenshot( self._screenShot, 200, 200, ";;gimage:") single_string_file_data += "\r" last_process_events = time.time() for line in self._gcode: single_string_file_data += line if time.time() > last_process_events + 0.05: QCoreApplication.processEvents() last_process_events = time.time() self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) self._post_part = QHttpPart() # self._post_part.setHeader(QNetworkRequest.ContentTypeHeader, b'application/octet-stream') self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) post_request = QNetworkRequest( QUrl("http://%s/upload?X-Filename=%s" % (self._address, file_name))) post_request.setRawHeader(b'Content-Type', b'application/octet-stream') post_request.setRawHeader(b'Connection', b'keep-alive') self._post_reply = self._manager.post(post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._post_reply.sslErrors.connect(self._onUploadError) # Logger.log("d", "http://%s:80/upload?X-Filename=%s" % (self._address, file_name)) self._gcode = None except IOError as e: Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Send file to printer failed.")) self._error_message.show() self._update_timer.start() except Exception as e: self._update_timer.start() self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) def _printFile(self): self._sendCommand("M23 " + self._last_file_name) self._sendCommand("M24") 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.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() def _onUploadError(self, reply, sslerror): Logger.log("d", "Upload Error") def _setHeadPosition(self, x, y, z, speed): self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) def _setHeadX(self, x, speed): self._sendCommand("G0 X%s F%s" % (x, speed)) def _setHeadY(self, y, speed): self._sendCommand("G0 Y%s F%s" % (y, speed)) def _setHeadZ(self, z, speed): self._sendCommand("G0 Z%s F%s" % (z, speed)) def _homeHead(self): self._sendCommand("G28 X Y") def _homeBed(self): self._sendCommand("G28 Z") def _moveHead(self, x, y, z, speed): self._sendCommand( ["G91", "G0 X%s Y%s Z%s F%s" % (x, y, z, speed), "G90"]) def _update(self): if self._socket is not None and (self._socket.state() == 2 or self._socket.state() == 3): _send_data = "M105\r\nM997\r\n" if self.isBusy(): _send_data += "M994\r\nM992\r\nM27\r\n" while self._command_queue.qsize() > 0: _queue_data = self._command_queue.get() if "M23" in _queue_data: self._socket.writeData(_queue_data.encode()) continue if "M24" in _queue_data: self._socket.writeData(_queue_data.encode()) continue _send_data += _queue_data # Logger.log("d", "_send_data: \r\n%s" % _send_data) self._socket.writeData(_send_data.encode()) self._socket.flush() # self._socket.waitForReadyRead() else: Logger.log("d", "MKS wifi reconnecting") self.disconnect() self.connect() def _setJobState(self, job_state): if job_state == "abort": command = "M26" elif job_state == "print": if self._isPause: command = "M25" else: command = "M24" elif job_state == "pause": command = "M25" if command: self._sendCommand(command) @pyqtSlot() def cancelPrint(self): self._sendCommand("M26") @pyqtSlot() def pausePrint(self): if self.printers[0].state == "paused": self._sendCommand("M24") else: self._sendCommand("M25") @pyqtSlot() def resumePrint(self): self._sendCommand("M25") def on_read(self): if not self._socket: self.disconnect() return try: if not self._isConnect: self._isConnect = True if self._connection_state != UnifiedConnectionState.Connected: self._sendCommand("M20") self.setConnectionState( cast(ConnectionState, UnifiedConnectionState.Connected)) self.setConnectionText( i18n_catalog.i18nc("@info:status", "TFT Connect succeed")) # ss = str(self._socket.readLine().data(), encoding=sys.getfilesystemencoding()) # while self._socket.canReadLine(): # ss = str(self._socket.readLine().data(), encoding=sys.getfilesystemencoding()) # ss_list = ss.split("\r\n") if not self._printers: self._createPrinterList() printer = self.printers[0] while self._socket.canReadLine(): s = str(self._socket.readLine().data(), encoding=sys.getfilesystemencoding()) # Logger.log("d", "mks recv: "+s) s = s.replace("\r", "").replace("\n", "") # if time.time() - self.last_update_time > 10 or time.time() - self.last_update_time<-10: # Logger.log("d", "mks time:"+str(self.last_update_time)+str(time.time())) # self._sendCommand("M20") # self.last_update_time = time.time() if "T" in s and "B" in s and "T0" in s: t0_temp = s[s.find("T0:") + len("T0:"):s.find("T1:")] t1_temp = s[s.find("T1:") + len("T1:"):s.find("@:")] bed_temp = s[s.find("B:") + len("B:"):s.find("T0:")] t0_nowtemp = float(t0_temp[0:t0_temp.find("/")]) t0_targettemp = float(t0_temp[t0_temp.find("/") + 1:len(t0_temp)]) t1_nowtemp = float(t1_temp[0:t1_temp.find("/")]) t1_targettemp = float(t1_temp[t1_temp.find("/") + 1:len(t1_temp)]) bed_nowtemp = float(bed_temp[0:bed_temp.find("/")]) bed_targettemp = float(bed_temp[bed_temp.find("/") + 1:len(bed_temp)]) # cura 3.4 new api printer.updateBedTemperature(bed_nowtemp) printer.updateTargetBedTemperature(bed_targettemp) extruder = printer.extruders[0] extruder.updateTargetHotendTemperature(t0_targettemp) extruder.updateHotendTemperature(t0_nowtemp) # self._number_of_extruders = 1 # extruder = printer.extruders[1] # extruder.updateHotendTemperature(t1_nowtemp) # extruder.updateTargetHotendTemperature(t1_targettemp) # only on lower 3.4 # self._setBedTemperature(bed_nowtemp) # self._updateTargetBedTemperature(bed_targettemp) # if self._num_extruders > 1: # self._setHotendTemperature(1, t1_nowtemp) # self._updateTargetHotendTemperature(1, t1_targettemp) # self._setHotendTemperature(0, t0_nowtemp) # self._updateTargetHotendTemperature(0, t0_targettemp) continue if printer.activePrintJob is None: print_job = PrintJobOutputModel( output_controller=self._output_controller) printer.updateActivePrintJob(print_job) else: print_job = printer.activePrintJob if s.startswith("M997"): job_state = "offline" if "IDLE" in s: self._isPrinting = False self._isPause = False job_state = 'idle' elif "PRINTING" in s: self._isPrinting = True self._isPause = False job_state = 'printing' elif "PAUSE" in s: self._isPrinting = False self._isPause = True job_state = 'paused' print_job.updateState(job_state) printer.updateState(job_state) # self._updateJobState(job_state) continue # print_job.updateState('idle') # printer.updateState('idle') if s.startswith("M994"): if self.isBusy() and s.rfind("/") != -1: self._printing_filename = s[s.rfind("/") + 1:s.rfind(";")] else: self._printing_filename = "" print_job.updateName(self._printing_filename) # self.setJobName(self._printing_filename) continue if s.startswith("M992"): if self.isBusy(): tm = s[s.find("M992") + len("M992"):len(s)].replace( " ", "") mms = tm.split(":") self._printing_time = int(mms[0]) * 3600 + int( mms[1]) * 60 + int(mms[2]) else: self._printing_time = 0 # Logger.log("d", self._printing_time) print_job.updateTimeElapsed(self._printing_time) # self.setTimeElapsed(self._printing_time) # print_job.updateTimeTotal(self._printing_time) # self.setTimeTotal(self._printing_time) continue if s.startswith("M27"): if self.isBusy(): self._printing_progress = float( s[s.find("M27") + len("M27"):len(s)].replace( " ", "")) totaltime = self._printing_time / self._printing_progress * 100 else: self._printing_progress = 0 totaltime = self._printing_time * 100 # Logger.log("d", self._printing_time) # Logger.log("d", totaltime) # self.setProgress(self._printing_progress) print_job.updateTimeTotal(self._printing_time) print_job.updateTimeElapsed(self._printing_time * 2 - totaltime) continue if 'Begin file list' in s: self._sdFileList = True self.sdFiles = [] self.last_update_time = time.time() continue if 'End file list' in s: self._sdFileList = False continue if self._sdFileList: s = s.replace("\n", "").replace("\r", "") if s.lower().endswith("gcode") or s.lower().endswith( "gco") or s.lower.endswith("g"): self.sdFiles.append(s) continue except Exception as e: print(e) def _updateTargetBedTemperature(self, temperature): if self._target_bed_temperature == temperature: return False self._target_bed_temperature = temperature self.targetBedTemperatureChanged.emit() return True 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 _createPrinterList(self): printer = PrinterOutputModel( output_controller=self._output_controller, number_of_extruders=self._number_of_extruders) printer.updateName(self.name) self._printers = [printer] self.printersChanged.emit() def _onRequestFinished(self, reply): http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) self._isSending = True self._update_timer.start() self._sendCommand("M20") preferences = Application.getInstance().getPreferences() preferences.addPreference("mkswifi/autoprint", "True") # preferences.addPreference("mkswifi/savepath", "") # preferences.setValue("mkswifi/autoprint", str(self._progress_message.getOptionState())) if preferences.getValue("mkswifi/autoprint"): self._printFile() if not http_status_code: return def _onOptionStateChanged(self, optstate): preferences = Application.getInstance().getPreferences() preferences.setValue("mkswifi/autoprint", str(optstate)) def _cancelSendGcode(self, message_id, action_id): self._update_timer.start() self._isSending = False self._progress_message.hide() self._post_reply.abort() def CreateMKSController(self): Logger.log("d", "Creating additional ui components for mkscontroller.") # self.__additional_components_view = CuraApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"mkscontroller": self}) self.__additional_components_view = Application.getInstance( ).createQmlComponent(self._monitor_view_qml_path, {"manager": self}) # trlist = CuraApplication.getInstance()._additional_components # for comp in trlist: Logger.log("w", "create mkscontroller ") if not self.__additional_components_view: Logger.log("w", "Could not create ui components for tft35.") return def _onGlobalContainerChanged(self) -> None: self._global_container_stack = Application.getInstance( ).getGlobalContainerStack() definitions = self._global_container_stack.definition.findDefinitions( key="cooling") Logger.log("d", definitions[0].label)
class RepetierServerOutputDevice(PrinterOutputDevice): def __init__(self, key, address, port, properties): super().__init__(key) self._address = address self._port = port self._path = properties["path"] if "path" in properties else "/" self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None self._auto_print = True ## Todo: Hardcoded value now; we should probably read this from the machine definition and Repetier-Server. self._num_extruders_set = False self._num_extruders = 1 self._slug = "fanera1" self._api_version = "1" self._api_prefix = "printer/api/" + self._slug self._api_header = "X-Api-Key" self._api_key = None self._base_url = "http://%s:%d%s" % (self._address, self._port, self._path) self._api_url = self._base_url + self._api_prefix self._model_url = self._base_url + "printer/model/" + self._slug + "?a=upload" self._camera_url = "http://%s:8080/?action=snapshot" % self._address self.setPriority( 2 ) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print with Repetier-Server")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print with Repetier-Server")) self.setIconName("print") self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected to Repetier-Server on {0}").format( self._key)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None self._printer_reply = None self._print_job_request = None self._print_job_reply = None self._image_request = None self._image_reply = None self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None self._job_request = None self._job_reply = None self._command_request = None self._command_reply = 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._update_camera) self._camera_image_id = 0 self._camera_image = QImage() self._connection_state_before_timeout = None self._last_response_time = None self._last_request_time = None self._response_timeout_time = 5 self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. self._recreate_network_manager_count = 1 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 ## Set the API key of this Repetier-Server instance def setApiKey(self, api_key): self._api_key = api_key ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self): return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self): return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self): return self._base_url def _update_camera(self): ## Request new image url = QUrl(self._camera_url) self._image_request = QNetworkRequest(url) self._image_reply = self._manager.get(self._image_request) 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 # 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: self._recreate_network_manager_count += 1 Logger.log( "d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", 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.")) self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: Logger.log( "d", "Stopping post upload because the connection was lost." ) try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. 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 %s seconds, so it seems Repetier-Server is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with Repetier-Server was lost. Check your network-connections." )) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data urlString = self._api_url + "?a=stateList" #Logger.log("d","XXX URL: " + urlString) url = QUrl(urlString) self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) ## Request print_job data url = QUrl(self._api_url + "?a=listPrinter") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_reply = self._manager.get(self._job_request) def _createNetworkManager(self): if self._manager: self._manager.finished.disconnect(self._onRequestFinished) self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) def close(self): self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() self._camera_timer.stop() self._camera_image = QImage() self.newImage.emit() def requestWrite(self, node, file_name=None, filter_by_machine=False): self.writeStarted.emit(self) self._gcode = getattr( Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from the instance def connect(self): #self.close() # Ensure that previous connection (if any) is killed. global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update( ) # Manually trigger the first update, as we don't want to wait a few secs before it starts. Logger.log("d", "Connection with instance %s with ip %s started", self._key, self._address) self._update_timer.start() if parseBool( global_container_stack.getMetaDataEntry( "octoprint_show_camera", False)): self._update_camera() self._camera_timer.start() else: self._camera_timer.stop() self._camera_image = QImage() self.newImage.emit() self._last_response_time = None self.setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connecting to Repetier-Server on {0}").format( self._key)) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %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): # if job_state == "abort": # command = "cancel" # elif job_state == "print": # if self.jobState == "paused": # command = "pause" # else: # command = "start" # elif job_state == "pause": # command = "pause" urlString = "" if job_state == "abort": command = "cancel" urlString = self._api_url + '?a=stopJob' elif job_state == "print": if self.jobState == "paused": command = "pause" urlString = self._api_url + '?a=send&data={"cmd":"@pause"}' else: command = "start" urlString = self._api_url + '?a=continueJob' elif job_state == "pause": command = "pause" urlString = self._api_url + '?a=send&data={"cmd":"@pause"}' Logger.log("d", "XXX:Command:" + command) Logger.log("d", "XXX:Command:" + urlString) if urlString: url = QUrl(urlString) self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) # if command: # self._sendCommand(command) def startPrint(self): global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return self._auto_print = parseBool( global_container_stack.getMetaDataEntry("repetier_auto_print", True)) if self._auto_print: Application.getInstance().showPrintMonitor.emit(True) if self.jobState != "ready" and self.jobState != "": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Repetier-Server is printing. Unable to start a new job.")) self._error_message.show() return try: self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to Repetier-Server"), 0, False, -1) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" last_process_events = time() for line in self._gcode: single_string_file_data += line if time() > last_process_events + 0.05: # Ensure that the GUI keeps updated at least 20 times per second. QCoreApplication.processEvents() last_process_events = time() file_name = "%s.gcode" % Application.getInstance( ).getPrintInformation().jobName ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) # self._post_part = QHttpPart() # self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") # self._post_part.setBody(b"true") # self._post_multi_part.append(self._post_part) # if self._auto_print: # self._post_part = QHttpPart() # self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") # self._post_part.setBody(b"true") # self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) # destination = "local" # if parseBool(global_container_stack.getMetaDataEntry("octoprint_store_sd", False)): # destination = "sdcard" # TODO ?? url = QUrl(self._model_url + "&name=" + file_name) ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send data to Repetier-Server.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) def _sendCommand(self, command): url = QUrl(self._api_url + "job") self._command_request = QNetworkRequest(url) self._command_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = "{\"command\": \"%s\"}" % command self._command_reply = self._manager.post(self._command_request, data.encode()) Logger.log("d", "Sent command to Repetier-Server instance: %s", data) def _setTargetBedTemperature(self, temperature): Logger.log("d", "Setting bed temperature to %s", temperature) self._sendCommand("M140 S%s" % temperature) def _setTargetHotendTemperature(self, index, temperature): Logger.log("d", "Setting hotend %s temperature to %s", index, temperature) self._sendCommand("M104 T%s S%s" % (index, temperature)) def _setHeadPosition(self, x, y, z, speed): self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) def _setHeadX(self, x, speed): self._sendCommand("G0 X%s F%s" % (x, speed)) def _setHeadY(self, y, speed): self._sendCommand("G0 Y%s F%s" % (y, speed)) def _setHeadZ(self, z, speed): self._sendCommand("G0 Y%s F%s" % (z, speed)) def _homeHead(self): self._sendCommand("G28") def _homeBed(self): self._sendCommand("G28 Z") def _moveHead(self, x, y, z, speed): self._sendCommand("G91") self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) self._sendCommand("G90") ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state 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. if self._last_response_time: Logger.log( "d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return if reply.operation() == QNetworkAccessManager.GetOperation: if "stateList" in reply.url().toString( ): # Status update from /printer. if http_status_code == 200: if not self.acceptsCommands: self.setAcceptsCommands(True) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected to Repetier-Server on {0}").format( self._key)) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) if not self._num_extruders_set: self._num_extruders = 0 ## TODO # while "extruder" % self._num_extruders in json_data[self._slug]: # self._num_extruders = self._num_extruders + 1 # Reinitialise from PrinterOutputDevice to match the new _num_extruders # self._hotend_temperatures = [0] * self._num_extruders # self._target_hotend_temperatures = [0] * self._num_extruders # self._num_extruders_set = True # TODO # Check for hotend temperatures # for index in range(0, self._num_extruders): # temperature = json_data[self._slug]["extruder"]["tempRead"] # self._setHotendTemperature(index, temperature) temperature = json_data[ self._slug]["extruder"][0]["tempRead"] self._setHotendTemperature(0, temperature) bed_temperature = json_data[ self._slug]["heatedBed"]["tempRead"] #bed_temperature_set = json_data[self._slug]["heatedBed"]["tempSet"] self._setBedTemperature(bed_temperature) elif http_status_code == 401: self.setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Repetier-Server on {0} does not allow access to print" ).format(self._key)) else: pass # TODO: Handle errors elif "listPrinter" in reply.url().toString( ): # Status update from /job: if http_status_code == 200: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) for printer in json_data: if printer["slug"] == self._slug: job_name = printer["job"] self.setJobName(job_name) job_state = "offline" # if printer["state"]["flags"]["error"]: # job_state = "error" if printer["paused"] == "true": job_state = "paused" elif job_name != "none": job_state = "printing" self.setProgress(printer["done"]) elif job_name == "none": job_state = "ready" self._updateJobState(job_state) # progress = json_data["progress"]["completion"] # if progress: # self.setProgress(progress) # if json_data["progress"]["printTime"]: # self.setTimeElapsed(json_data["progress"]["printTime"]) # if json_data["progress"]["printTimeLeft"]: # self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) # elif json_data["job"]["estimatedPrintTime"]: # self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"])) # elif progress > 0: # self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100)) # else: # self.setTimeTotal(0) # else: # self.setTimeElapsed(0) # self.setTimeTotal(0) # self.setJobName(json_data["job"]["file"]["name"]) else: pass # TODO: Handle errors elif "snapshot" in reply.url().toString(): # Update from camera: if http_status_code == 200: self._camera_image.loadFromData(reply.readAll()) self.newImage.emit() else: pass # TODO: Handle errors elif reply.operation() == QNetworkAccessManager.PostOperation: if "model" in reply.url().toString( ): # Result from /files command: if http_status_code == 201: Logger.log( "d", "Resource created on Repetier-Server instance: %s", reply.header( QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) modelList = json_data["data"] lastModel = modelList[len(modelList) - 1] # Logger.log("d", "XXX1:len"+str(len(modelList))) # Logger.log("d", "XXX1:lastModel"+str(lastModel)) modelId = lastModel["id"] # "http://%s:%d%s" % (self._address, self._port, self._path) urlString = self._api_url + '?a=copyModel&data={"id": %s}' % ( modelId) Logger.log("d", "XXX1: modelId: " + str(modelId)) url = QUrl(urlString) self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) Logger.log("d", "XXX1: modelId: " + str(urlString)) reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not self._auto_print: location = reply.header(QNetworkRequest.LocationHeader) if location: file_name = QUrl( reply.header(QNetworkRequest.LocationHeader). toString()).fileName() message = Message( i18n_catalog.i18nc( "@info:status", "Saved to Repetier-Server as {0}").format( file_name)) else: message = Message( i18n_catalog.i18nc("@info:status", "Saved to Repetier-Server")) message.addAction( "open_browser", i18n_catalog.i18nc("@action:button", "Open Repetier-Server..."), "globe", i18n_catalog.i18nc( "@info:tooltip", "Open the Repetier-Server web interface")) message.actionTriggered.connect( self._onMessageActionTriggered) message.show() elif "job" in reply.url().toString(): # Result from /job command: if http_status_code == 204: Logger.log("d", "Repetier-Server command accepted") else: pass # TODO: Handle errors else: Logger.log( "d", "RepetierServerOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: # 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() progress = bytes_sent / bytes_total * 100 if progress < 100: if progress > self._progress_message.getProgress(): self._progress_message.setProgress(progress) else: self._progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Storing data on Repetier-Server"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self._base_url))
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 <filename>{file_name}</filename> 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())
def _sendRequest(self, path, name=None, data=None, dataIsJSON=False, on_success=None, on_error=None): url = self._url + path headers = { 'User-Agent': 'Cura Plugin Moonraker', 'Accept': 'application/json, text/plain', 'Connection': 'keep-alive' } if self._api_key: headers['X-API-Key'] = self._api_key postData = data if data is not None: if not dataIsJSON: # Create multi_part request parts = QHttpMultiPart(QHttpMultiPart.FormDataType) part_file = QHttpPart() part_file.setHeader( QNetworkRequest.ContentDispositionHeader, QVariant('form-data; name="file"; filename="/' + name + '"')) part_file.setHeader(QNetworkRequest.ContentTypeHeader, QVariant('application/octet-stream')) part_file.setBody(data) parts.append(part_file) part_root = QHttpPart() part_root.setHeader(QNetworkRequest.ContentDispositionHeader, QVariant('form-data; name="root"')) part_root.setBody(b"gcodes") parts.append(part_root) if self._startPrint: part_print = QHttpPart() part_print.setHeader( QNetworkRequest.ContentDispositionHeader, QVariant('form-data; name="print"')) part_print.setBody(b"true") parts.append(part_print) headers[ 'Content-Type'] = 'multipart/form-data; boundary=' + str( parts.boundary().data(), encoding='utf-8') postData = parts else: # postData is JSON headers['Content-Type'] = 'application/json' self.application.getHttpRequestManager().post( url, headers, postData, callback=on_success, error_callback=on_error if on_error else self._onNetworkError, upload_progress_callback=self._onUploadProgress if not dataIsJSON else None) else: self.application.getHttpRequestManager().get( url, headers, callback=on_success, error_callback=on_error if on_error else self._onNetworkError)
def uploadGCode(self, data): try: job_name = Application.getInstance().getPrintInformation( ).jobName.strip() if job_name == "": job_name = "untitled_print" global_stack = Application.getInstance().getGlobalContainerStack() machine_manager = Application.getInstance().getMachineManager() cura_printer_type = machine_manager.activeDefinitionId printer_type = ConnectPrinterIdTranslation.curaPrinterIdToConnect( cura_printer_type) # Fall back to marlin or makerbot generic if printer is not supported on WiFi-Box if printer_type is None: gcode_flavor = global_stack.getProperty( "machine_gcode_flavor", "value") if gcode_flavor == "RepRap (Marlin/Sprinter)": printer_type = "marlin_generic" elif gcode_flavor == "MakerBot": printer_type = "makerbot_generic" else: printer_type = cura_printer_type sliceInfo = { 'printer': { 'type': printer_type, 'title': global_stack.getName() }, 'material': { 'type': global_stack.material.getId(), 'title': global_stack.material.getName() }, 'filamentThickness': global_stack.getProperty("material_diameter", "value"), 'temperature': global_stack.getProperty("material_print_temperature", "value"), 'name': job_name } gcode_list = getattr( Application.getInstance().getController().getScene(), "gcode_list") gcode = ";%s\n" % json.dumps(sliceInfo) for line in gcode_list: gcode += line multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) for prop_name, prop_value in data["data"]["reservation"][ "fields"].items(): part = QHttpPart() part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"%s\"" % prop_name) part.setBody(prop_value.encode()) multi_part.append(part) gcode_part = QHttpPart() gcode_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"") gcode_part.setBody(gcode.encode()) multi_part.append(gcode_part) upload_url = QUrl(data["data"]["reservation"]["url"]) Logger.log("d", "{}", upload_url) self._post_reply = self._manager.post(QNetworkRequest(upload_url), multi_part) self._post_reply.uploadProgress.connect(self._onProgress) self._post_reply.error.connect(self._onNetworkError) self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to Doodle3D Connect"), 0, False, -1) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._onMessageActionTriggered) self._progress_message.show() multi_part.setParent(self._post_reply) except Exception as e: self._progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc( "@info:status", "Unable to send data to Doodle3D Connect. Is another job still active?" )) self._progress_message.show() Logger.log( "e", "An exception occured during G-code upload: %s" % str(e))
class OctoPrintOutputDevice(PrinterOutputDevice): def __init__(self, key, address, properties): super().__init__(key) self._address = address self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None ## Todo: Hardcoded value now; we should probably read this from the machine definition and octoprint. self._num_extruders = 1 self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders self._api_version = "1" self._api_prefix = "/api/" self._api_header = "X-Api-Key" self._api_key = None self.setName(key) self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with OctoPrint")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint")) self.setIconName("print") # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None self._printer_reply = None self._print_job_request = None self._print_job_reply = None self._image_request = None self._image_reply = None self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None self._job_request = None self._job_reply = None self._progress_message = None self._error_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._update_camera) self._camera_image_id = 0 self._camera_image = QImage() def getProperties(self): return self._properties ## 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 ## Set the API key of this OctoPrint instance def setApiKey(self, api_key): self._api_key = api_key ## Name of the printer (as returned from the zeroConf properties) @pyqtProperty(str, constant = True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this printer @pyqtProperty(str, constant=True) def ipAddress(self): return self._address def _update_camera(self): ## Request new image url = QUrl("http://" + self._address + ":8080/?action=snapshot") self._image_request = QNetworkRequest(url) self._image_reply = self._manager.get(self._image_request) def _update(self): ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) ## Request print_job data url = QUrl("http://" + self._address + self._api_prefix + "job") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_reply = self._manager.get(self._job_request) def close(self): self.setConnectionState(ConnectionState.closed) self._update_timer.stop() self._camera_timer.stop() def requestWrite(self, node, file_name = None, filter_by_machine = False): self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from printer def connect(self): self.setConnectionState(ConnectionState.connecting) self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. self._update_camera() Logger.log("d", "Connection with printer %s with ip %s started", self._key, self._address) self._update_timer.start() self._camera_timer.start() newImage = pyqtSignal() @pyqtProperty(QUrl, notify = newImage) def cameraImage(self): self._camera_image_id += 1 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): url = QUrl("http://" + self._address + self._api_prefix + "job") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") if job_state == "abort": command = "cancel" elif job_state == "print": if self.jobState == "paused": command = "pause" else: command = "start" elif job_state == "pause": command = "pause" data = "{\"command\": \"%s\"}" % command self._job_reply = self._manager.post(self._job_request, data.encode()) Logger.log("d", "Sent command to OctoPrint instance: %s", data) def startPrint(self): if self.jobState != "ready" and self.jobState != "": self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer is printing. Unable to start a new job.")) self._error_message.show() return try: self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" for line in self._gcode: single_string_file_data += line ## TODO: Use correct file name (we use placeholder now) file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) url = QUrl("http://" + self._address + self._api_prefix + "files/local") ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None 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?")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log("e", "An exception occurred in network connection: %s" % str(e)) ## Handler for all requests that have finished. def _onFinished(self, reply): http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString(): # Status update from /printer. if http_status_code == 200: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) # Check for hotend temperatures for index in range(0, self._num_extruders): temperature = json_data["temperature"]["tool%d" % index]["actual"] self._setHotendTemperature(index, temperature) bed_temperature = json_data["temperature"]["bed"]["actual"] self._setBedTemperature(bed_temperature) printer_state = "offline" if json_data["state"]["flags"]["error"]: printer_state = "error" elif json_data["state"]["flags"]["paused"]: printer_state = "paused" elif json_data["state"]["flags"]["printing"]: printer_state = "printing" elif json_data["state"]["flags"]["ready"]: printer_state = "ready" self._updateJobState(printer_state) else: pass # TODO: Handle errors elif "job" in reply.url().toString(): # Status update from /job: if http_status_code == 200: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) progress = json_data["progress"]["completion"] if progress: self.setProgress(progress) if json_data["progress"]["printTime"]: self.setTimeElapsed(json_data["progress"]["printTime"]) if json_data["progress"]["printTimeLeft"]: self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) elif json_data["job"]["estimatedPrintTime"]: self.setTimeTotal(json_data["job"]["estimatedPrintTime"]) elif progress > 0: self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100)) else: self.setTimeTotal(0) else: self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName(json_data["job"]["file"]["name"]) else: pass # TODO: Handle errors elif "snapshot" in reply.url().toString(): # Update from camera: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: self._camera_image.loadFromData(reply.readAll()) self.newImage.emit() else: pass # TODO: Handle errors elif reply.operation() == QNetworkAccessManager.PostOperation: if "files" in reply.url().toString(): # Result from /files command: if http_status_code == 201: Logger.log("d", "Resource created on OctoPrint instance: %s", reply.header(QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() elif "job" in reply.url().toString(): # Result from /job command: if http_status_code == 204: Logger.log("d", "Octoprint command accepted") else: pass # TODO: Handle errors else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: self._progress_message.setProgress(0)
class SculptoPrintOutputDevice(PrinterOutputDevice): def __init__(self, key, address, port, properties): super().__init__(key) self._address = address self._port = port self._path = properties["path"] if "path" in properties else "/" self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None self._auto_print = True ## We start with a single extruder, but update this when we get data from Sculptoprint self._num_extruders = 1 self._api_version = "1" self._api_prefix = "api/" self._api_header = "X-Api-Key" self._api_key = None self._base_url = "http://%s:%d/" % (self._address, self._port) self._api_url = self._base_url + self._api_prefix self.setPriority( 2 ) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print on Sculpto")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print on Sculpto")) self.setIconName("print") self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected to Sculpto on {0}").format( self._key)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None self._printer_reply = None self._print_job_request = None self._print_job_reply = None self._image_request = None self._image_reply = None self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None self._job_request = None self._job_reply = None self._command_request = None self._command_reply = 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._connection_state_before_timeout = None self._is_printing = False self.estimated_total = 0 self.starting_time = 0 self._last_response_time = None self._last_request_time = None self._response_timeout_time = 5 self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec. self._recreate_network_manager_count = 1 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 ## Set the API key of this SculptoPrint instance def setApiKey(self, api_key): self._api_key = api_key ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def SculptoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self): return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self): return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self): return self._base_url 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 # 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: self._recreate_network_manager_count += 1 Logger.log( "d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", 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.")) self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: Logger.log( "d", "Stopping post upload because the connection was lost." ) try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. 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 %s seconds, so it seems SculptoPrint is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with Sculpto was lost. Check your network-connections." )) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data url = QUrl(self._base_url + "temperature") self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) ## Request print_job data url = QUrl(self._base_url + "progress") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_reply = self._manager.get(self._job_request) def _createNetworkManager(self): if self._manager: self._manager.finished.disconnect(self._onRequestFinished) self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) def close(self): self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() self.newImage.emit() def requestWrite(self, node, file_name=None, filter_by_machine=False): self.writeStarted.emit(self) self._gcode = getattr( Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from the instance def connect(self): #self.close() # Ensure that previous connection (if any) is killed. global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update( ) # Manually trigger the first update, as we don't want to wait a few secs before it starts. Logger.log("d", "Connection with instance %s with ip %s started", self._key, self._address) self._update_timer.start() self._last_response_time = None self.setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connecting to Sculpto on {0}").format( self._key)) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %s with ip %s stopped", self._key, self._address) self.close() newImage = pyqtSignal() def _setJobState(self, job_state): if job_state == "abort": Logger.log("d", "Should abort!") return #command = "cancel" elif job_state == "print": if self.jobState == "paused": command = "pause" else: command = "start" elif job_state == "pause": command = "pause" if command: self._sendCommand(command) def stopPrint(self): self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"cool\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) url = QUrl(self._base_url + "stop_print") ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) def startPrint(self): global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return self._auto_print = parseBool( global_container_stack.getMetaDataEntry("sculptoprint_auto_print", True)) if self._auto_print: Application.getInstance().showPrintMonitor.emit(True) if self.jobState != "ready" and self.jobState != "": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Sculpto is printing. Unable to start a new job.")) self._error_message.show() return try: self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to Sculpto"), 0, False, -1) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" last_process_events = time() for line in self._gcode: single_string_file_data += line if time() > last_process_events + 0.05: # Ensure that the GUI keeps updated at least 20 times per second. QCoreApplication.processEvents() last_process_events = time() file_name = "%s.gcode" % Application.getInstance( ).getPrintInformation().jobName ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (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(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) url = QUrl(self._base_url + "upload_and_print") self.setJobName(file_name) ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send data to SculptoPrint.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) def _sendCommand(self, command): url = QUrl(self._api_url + "job") self._command_request = QNetworkRequest(url) self._command_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = "{\"command\": \"%s\"}" % command self._command_reply = self._manager.post(self._command_request, data.encode()) Logger.log("d", "Sent command to SculptoPrint instance: %s", data) def _setTargetBedTemperature(self, temperature): Logger.log("d", "Setting bed temperature to %s", temperature) self._sendCommand("M140 S%s" % temperature) def _setTargetHotendTemperature(self, index, temperature): Logger.log("d", "Setting hotend %s temperature to %s", index, temperature) self._sendCommand("M104 T%s S%s" % (index, temperature)) def _setHeadPosition(self, x, y, z, speed): self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) def _setHeadX(self, x, speed): self._sendCommand("G0 X%s F%s" % (x, speed)) def _setHeadY(self, y, speed): self._sendCommand("G0 Y%s F%s" % (y, speed)) def _setHeadZ(self, z, speed): self._sendCommand("G0 Y%s F%s" % (z, speed)) def _homeHead(self): self._sendCommand("G28") def _homeBed(self): self._sendCommand("G28 Z") def _moveHead(self, x, y, z, speed): self._sendCommand("G91") self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) self._sendCommand("G90") ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state 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. if self._last_response_time: Logger.log( "d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return Logger.log("w", "Got response") if reply.operation() == QNetworkAccessManager.GetOperation: if "temperature" in reply.url().toString( ): # Status update from /temperature. if http_status_code == 200: if not self.acceptsCommands: self.setAcceptsCommands(True) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected to SculptoPrint on {0}").format( self._key)) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) self._setHotendTemperature(0, json_data['payload']) ''' job_state = "offline" if json_data["state"]["flags"]["error"]: job_state = "error" elif json_data["state"]["flags"]["paused"]: job_state = "paused" elif json_data["state"]["flags"]["printing"]: job_state = "printing" elif json_data["state"]["flags"]["ready"]: job_state = "ready" self._updateJobState(job_state) ''' elif http_status_code == 401: self.setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Sculpto on {0} does not allow access to print"). format(self._key)) else: pass # TODO: Handle errors elif "progress" in reply.url().toString( ): # Status update from /progress: if http_status_code == 500: self.setTimeElapsed(0) self.setTimeTotal(0) self._updateJobState("ready") self.setProgress(0) self.setJobName("Waiting for Print") self._is_printing = False if http_status_code == 200: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) Logger.log("d", "Progress: {0}".format(json_data["payload"])) self.setProgress(json_data["payload"]) self._updateJobState("printing") self._is_printing = True self.setTimeElapsed(time() - self.starting_time) ''' progress = json_data["progress"]["completion"] if progress: self.setProgress(progress) if json_data["progress"]["printTime"]: self.setTimeElapsed(json_data["progress"]["printTime"]) if json_data["progress"]["printTimeLeft"]: self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) elif json_data["job"]["estimatedPrintTime"]: self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"])) elif progress > 0: self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100)) else: self.setTimeTotal(0) else: self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName(json_data["job"]["file"]["name"]) ''' else: pass # TODO: Handle errors elif reply.operation() == QNetworkAccessManager.PostOperation: if "upload_and_print" in reply.url().toString( ): # Result from /upload_and_print command: if http_status_code == 201: Logger.log("d", "Successfully uploaded and printing!") print_information = Application.getInstance( ).getPrintInformation().currentPrintTime self.estimated_total = int( print_information.getDisplayString(0)) self.starting_time = time() self.setTimeTotal(self.estimated_total) self.setTimeElapsed(0) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not self._auto_print: location = reply.header(QNetworkRequest.LocationHeader) if location: file_name = QUrl( reply.header(QNetworkRequest.LocationHeader). toString()).fileName() message = Message( i18n_catalog.i18nc( "@info:status", "Saved to SculptoPrint as {0}").format( file_name)) else: message = Message( i18n_catalog.i18nc("@info:status", "Saved to SculptoPrint")) message.addAction( "open_browser", i18n_catalog.i18nc("@action:button", "Open SculptoPrint..."), "globe", i18n_catalog.i18nc( "@info:tooltip", "Open the SculptoPrint web interface")) message.actionTriggered.connect( self._onMessageActionTriggered) message.show() elif "stop_print" in reply.url().toString( ): # Result from /stop_print command: if http_status_code == 200: Logger.log("d", "Printing stopped") self._updateJobState("ready") self.setProgress(0) self._is_printing = False else: pass # TODO: Handle errors else: Logger.log( "d", "SculptoPrintOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: # 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() progress = bytes_sent / bytes_total * 100 if progress < 100: if progress > self._progress_message.getProgress(): self._progress_message.setProgress(progress) else: self._progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc( "@info:status", "Storing gcode on Sculpto and starting print"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self._base_url))
def genReqStr(value: Dict[str, Any], prefix="", multiPart=None) -> QHttpMultiPart: if not multiPart: multiPart = QHttpMultiPart(QHttpMultiPart.FormDataType) if isinstance( value, (QtCore.QFile, io.BufferedReader)): # file content must be a QFile object # FIXME: This is broken. IoBufferedReader will be read to memory entirely - not streamed!! if "name" in dir(value): fileName = value.name elif "fileName" in dir(value): fileName = value.fileName() try: mimetype, encoding = mimetypes.guess_type(fileName, strict=False) mimetype = mimetype or "application/octet-stream" except: mimetype = "application/octet-stream" filePart = QHttpPart() filePart.setHeader(QNetworkRequest.ContentTypeHeader, mimetype) filePart.setHeader( QNetworkRequest.ContentDispositionHeader, 'form-data; name="{0}"; filename="{1}"'.format( prefix, os.path.basename(fileName))) if isinstance(value, io.BufferedReader): filePart.setBody(value.read()) else: filePart.setBodyDevice(value) value.setParent(multiPart) multiPart.append(filePart) elif isinstance(value, list): if not value: otherPart = QHttpPart() otherPart.setHeader(QNetworkRequest.ContentTypeHeader, "application/octet-stream") otherPart.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="{0}"'.format(prefix)) otherPart.setBody(b"") multiPart.append(otherPart) elif any([isinstance(x, dict) for x in value]): for idx, v in enumerate(value): NetworkService.genReqStr( v, (prefix + "." if prefix else "") + str(idx), multiPart) else: for val in value: logger.debug("serializing param item %r of list value %r", val, prefix) textPart = QHttpPart() textPart.setHeader(QNetworkRequest.ContentTypeHeader, "application/octet-stream") textPart.setHeader( QNetworkRequest.ContentDispositionHeader, 'form-data; name="{0}"'.format(prefix)) textPart.setBody(str(val).encode("utf-8")) multiPart.append(textPart) elif isinstance(value, dict): if prefix: prefix += "." for k, v in value.items(): NetworkService.genReqStr(v, prefix + k, multiPart) #elif value is None: #return multiPart else: if value is None: value = "" otherPart = QHttpPart() otherPart.setHeader(QNetworkRequest.ContentTypeHeader, "application/octet-stream") otherPart.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="{0}"'.format(prefix)) otherPart.setBody(str(value).encode("utf-8")) multiPart.append(otherPart) return multiPart
class OctoPrintOutputDevice(NetworkedPrinterOutputDevice): def __init__(self, instance_id: str, address: str, port: int, properties: dict, parent=None) -> None: super().__init__(device_id=instance_id, address=address, properties=properties, parent=parent) self._address = address self._port = port self._path = properties.get(b"path", b"/").decode("utf-8") if self._path[-1:] != "/": self._path += "/" self._id = instance_id self._properties = properties # Properties dict as provided by zero conf self._gcode = [] self._auto_print = True self._forced_queue = False # We start with a single extruder, but update this when we get data from octoprint self._number_of_extruders_set = False self._number_of_extruders = 1 # Try to get version information from plugin.json plugin_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "plugin.json") try: with open(plugin_file_path) as plugin_file: plugin_info = json.load(plugin_file) plugin_version = plugin_info["version"] except: # The actual version info is not critical to have so we can continue plugin_version = "Unknown" Logger.logException( "w", "Could not get version information for the plugin") self._user_agent_header = "User-Agent".encode() self._user_agent = ( "%s/%s %s/%s" % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion(), "OctoPrintPlugin", Application.getInstance().getVersion()) ) # NetworkedPrinterOutputDevice defines this as string, so we encode this later self._api_prefix = "api/" self._api_header = "X-Api-Key".encode() self._api_key = None self._protocol = "https" if properties.get( b'useHttps') == b"true" else "http" self._base_url = "%s://%s:%d%s" % (self._protocol, self._address, self._port, self._path) self._api_url = self._base_url + self._api_prefix self._basic_auth_header = "Authorization".encode() self._basic_auth_data = None basic_auth_username = properties.get(b"userName", b"").decode("utf-8") basic_auth_password = properties.get(b"password", b"").decode("utf-8") if basic_auth_username and basic_auth_password: data = base64.b64encode( ("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8") self._basic_auth_data = ("basic %s" % data).encode() self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") name = self._id matches = re.search(r"^\"(.*)\"\._octoprint\._tcp.local$", name) if matches: name = matches.group(1) self.setPriority( 2 ) # Make sure the output device gets selected above local file output self.setName(name) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print with OctoPrint")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint")) self.setIconName("print") self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format( self._id)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._settings_reply = None self._printer_reply = None self._job_reply = None self._command_reply = None self._post_reply = None self._post_multi_part = None self._progress_message = None self._error_message = None self._connection_message = None self._queued_gcode_commands = [] # type: List[str] self._queued_gcode_timer = QTimer() self._queued_gcode_timer.setInterval(0) self._queued_gcode_timer.setSingleShot(True) self._queued_gcode_timer.timeout.connect(self._sendQueuedGcode) 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_mirror = "" self._camera_rotation = 0 self._camera_url = "" self._camera_shares_proxy = False self._sd_supported = False self._plugin_data = {} #type: Dict[str, Any] self._connection_state_before_timeout = None self._last_response_time = None self._last_request_time = None self._response_timeout_time = 5 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._output_controller = GenericOutputController(self) 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 getId(self): return self._id ## Set the API key of this OctoPrint instance def setApiKey(self, api_key): self._api_key = api_key.encode() ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self): return self._name ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address ## IPadress of this instance # Overridden from NetworkedPrinterOutputDevice because OctoPrint does not # send the ip address with zeroconf @pyqtProperty(str, constant=True) def address(self): return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self): return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self): return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self): return self._base_url cameraOrientationChanged = pyqtSignal() @pyqtProperty("QVariantMap", notify=cameraOrientationChanged) def cameraOrientation(self): return { "mirror": self._camera_mirror, "rotation": self._camera_rotation, } 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 # 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: self._recreate_network_manager_count += 1 Logger.log( "d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", 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.")) self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: Logger.log( "d", "Stopping post upload because the connection was lost." ) try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. 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 %s seconds, so it seems OctoPrint is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with OctoPrint was lost. Check your network-connections." )) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data self._printer_reply = self._manager.get( self._createApiRequest("printer")) ## Request print_job data self._job_reply = self._manager.get(self._createApiRequest("job")) def _createNetworkManager(self): if self._manager: self._manager.finished.disconnect(self._onRequestFinished) self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) def _createApiRequest(self, end_point): request = QNetworkRequest(QUrl(self._api_url + end_point)) request.setRawHeader(self._user_agent_header, self._user_agent.encode()) request.setRawHeader(self._api_header, self._api_key) if self._basic_auth_data: request.setRawHeader(self._basic_auth_header, self._basic_auth_data) return request def close(self): self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() def requestWrite(self, node, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self.writeStarted.emit(self) active_build_plate = Application.getInstance().getMultiBuildPlateModel( ).activeBuildPlate scene = Application.getInstance().getController().getScene() gcode_dict = getattr(scene, "gcode_dict", None) if not gcode_dict: return self._gcode = gcode_dict.get(active_build_plate, None) self.startPrint() ## Start requesting data from the instance def connect(self): self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update( ) # Manually trigger the first update, as we don't want to wait a few secs before it starts. Logger.log("d", "Connection with instance %s with url %s started", self._id, self._base_url) self._update_timer.start() self._last_response_time = None self._setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connecting to OctoPrint on {0}").format( self._id)) ## Request 'settings' dump self._settings_reply = self._manager.get( self._createApiRequest("settings")) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %s with url %s stopped", self._id, self._base_url) self.close() def pausePrint(self): self._sendJobCommand("pause") def resumePrint(self): if not self._printers[0].activePrintJob: return if self._printers[0].activePrintJob.state == "paused": self._sendJobCommand("pause") else: self._sendJobCommand("start") def cancelPrint(self): self._sendJobCommand("cancel") def startPrint(self): global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return if self._error_message: self._error_message.hide() self._error_message = None if self._progress_message: self._progress_message.hide() self._progress_message = None self._auto_print = parseBool( global_container_stack.getMetaDataEntry("octoprint_auto_print", True)) self._forced_queue = False if self.activePrinter.state not in ["idle", ""]: Logger.log( "d", "Tried starting a print, but current state is %s" % self.activePrinter.state) if not self._auto_print: # allow queueing the job even if OctoPrint is currently busy if autoprinting is disabled self._error_message = None elif self.activePrinter.state == "offline": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "The printer is offline. Unable to start a new job.")) else: self._error_message = Message( i18n_catalog.i18nc( "@info:status", "OctoPrint is busy. Unable to start a new job.")) if self._error_message: self._error_message.addAction( "Queue", i18n_catalog.i18nc("@action:button", "Queue job"), None, i18n_catalog.i18nc( "@action:tooltip", "Queue this print job so it can be printed later")) self._error_message.actionTriggered.connect(self._queuePrint) self._error_message.show() return self._startPrint() def _queuePrint(self, message_id, action_id): if self._error_message: self._error_message.hide() self._forced_queue = True self._startPrint() def _startPrint(self): if self._auto_print and not self._forced_queue: Application.getInstance().getController().setActiveStage( "MonitorStage") # cancel any ongoing preheat timer before starting a print try: self._printers[0].stopPreheatTimers() except AttributeError: # stopPreheatTimers was added after Cura 3.3 beta pass self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect(self._cancelSendGcode) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" last_process_events = time() for line in self._gcode: single_string_file_data += line if time() > last_process_events + 0.05: # Ensure that the GUI keeps updated at least 20 times per second. QCoreApplication.processEvents() last_process_events = time() job_name = Application.getInstance().getPrintInformation( ).jobName.strip() if job_name is "": job_name = "untitled_print" file_name = "%s.gcode" % job_name ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") post_part.setBody(b"true") self._post_multi_part.append(post_part) if self._auto_print and not self._forced_queue: post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") post_part.setBody(b"true") self._post_multi_part.append(post_part) post_part = QHttpPart() post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(post_part) destination = "local" if self._sd_supported and parseBool(Application.getInstance( ).getGlobalContainerStack().getMetaDataEntry("octoprint_store_sd", False)): destination = "sdcard" try: ## Post request + data post_request = self._createApiRequest("files/" + destination) self._post_reply = self._manager.post(post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) self._gcode = [] def _cancelSendGcode(self, message_id, action_id): if self._post_reply: Logger.log("d", "Stopping upload because the user pressed cancel.") try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._post_reply = None if self._progress_message: self._progress_message.hide() def sendCommand(self, command): self._queued_gcode_commands.append(command) self._queued_gcode_timer.start() # Send gcode commands that are queued in quick succession as a single batch def _sendQueuedGcode(self): if self._queued_gcode_commands: self._sendCommandToApi("printer/command", self._queued_gcode_commands) Logger.log("d", "Sent gcode command to OctoPrint instance: %s", self._queued_gcode_commands) self._queued_gcode_commands = [] def _sendJobCommand(self, command): self._sendCommandToApi("job", command) Logger.log("d", "Sent job command to OctoPrint instance: %s", command) def _sendCommandToApi(self, end_point, commands): command_request = self._createApiRequest(end_point) command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") if isinstance(commands, list): data = json.dumps({"commands": commands}) else: data = json.dumps({"command": commands}) self._command_reply = self._manager.post(command_request, data.encode()) ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state 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. if self._last_response_time: Logger.log( "d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return error_handled = False if reply.operation() == QNetworkAccessManager.GetOperation: if self._api_prefix + "printer" in reply.url().toString( ): # Status update from /printer. if not self._printers: self._createPrinterList() # An OctoPrint instance has a single printer. printer = self._printers[0] if http_status_code == 200: if not self.acceptsCommands: self._setAcceptsCommands(True) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected to OctoPrint on {0}").format( self._id)) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") json_data = {} if "temperature" in json_data: if not self._number_of_extruders_set: self._number_of_extruders = 0 while "tool%d" % self._number_of_extruders in json_data[ "temperature"]: self._number_of_extruders += 1 if self._number_of_extruders > 1: # Recreate list of printers to match the new _number_of_extruders self._createPrinterList() printer = self._printers[0] if self._number_of_extruders > 0: self._number_of_extruders_set = True # Check for hotend temperatures for index in range(0, self._number_of_extruders): extruder = printer.extruders[index] if ("tool%d" % index) in json_data["temperature"]: hotend_temperatures = json_data["temperature"][ "tool%d" % index] extruder.updateTargetHotendTemperature( hotend_temperatures["target"]) extruder.updateHotendTemperature( hotend_temperatures["actual"]) else: extruder.updateTargetHotendTemperature(0) extruder.updateHotendTemperature(0) if "bed" in json_data["temperature"]: bed_temperatures = json_data["temperature"]["bed"] actual_temperature = bed_temperatures[ "actual"] if bed_temperatures[ "actual"] is not None else -1 printer.updateBedTemperature(actual_temperature) target_temperature = bed_temperatures[ "target"] if bed_temperatures[ "target"] is not None else -1 printer.updateTargetBedTemperature( target_temperature) else: printer.updateBedTemperature(-1) printer.updateTargetBedTemperature(0) printer_state = "offline" if "state" in json_data: flags = json_data["state"]["flags"] if flags["error"] or flags["closedOrError"]: printer_state = "error" elif flags["paused"] or flags["pausing"]: printer_state = "paused" elif flags["printing"]: printer_state = "printing" elif flags["cancelling"]: printer_state = "aborted" elif flags["ready"] or flags["operational"]: printer_state = "idle" printer.updateState(printer_state) elif http_status_code == 401: printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "OctoPrint on {0} does not allow access to print"). format(self._id)) error_handled = True elif http_status_code == 409: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "The printer connected to OctoPrint on {0} is not operational" ).format(self._id)) error_handled = True else: printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") Logger.log("w", "Received an unexpected returncode: %d", http_status_code) elif self._api_prefix + "job" in reply.url().toString( ): # Status update from /job: if not self._printers: return # Ignore the data for now, we don't have info about a printer yet. printer = self._printers[0] if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") json_data = {} if printer.activePrintJob is None: print_job = PrintJobOutputModel( output_controller=self._output_controller) printer.updateActivePrintJob(print_job) else: print_job = printer.activePrintJob print_job_state = "offline" if "state" in json_data: if json_data["state"] == "Error": print_job_state = "error" elif json_data["state"] == "Pausing": print_job_state = "pausing" elif json_data["state"] == "Paused": print_job_state = "paused" elif json_data["state"] == "Printing": print_job_state = "printing" elif json_data["state"] == "Cancelling": print_job_state = "abort" elif json_data["state"] == "Operational": print_job_state = "ready" printer.updateState("idle") print_job.updateState(print_job_state) print_time = json_data["progress"]["printTime"] if print_time: print_job.updateTimeElapsed(print_time) if json_data["progress"][ "completion"]: # not 0 or None or "" print_job.updateTimeTotal( print_time / (json_data["progress"]["completion"] / 100)) else: print_job.updateTimeTotal(0) else: print_job.updateTimeElapsed(0) print_job.updateTimeTotal(0) print_job.updateName(json_data["job"]["file"]["name"]) else: pass # See generic error handler below elif self._api_prefix + "settings" in reply.url().toString( ): # OctoPrint settings dump from /settings: if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from octoprint instance.") json_data = {} if "feature" in json_data and "sdSupport" in json_data[ "feature"]: self._sd_supported = json_data["feature"]["sdSupport"] if "webcam" in json_data and "streamUrl" in json_data[ "webcam"]: self._camera_shares_proxy = False stream_url = json_data["webcam"]["streamUrl"] if not stream_url: #empty string or None self._camera_url = "" elif stream_url[:4].lower() == "http": # absolute uri self._camera_url = stream_url elif stream_url[:2] == "//": # protocol-relative self._camera_url = "%s:%s" % (self._protocol, stream_url) elif stream_url[: 1] == ":": # domain-relative (on another port) self._camera_url = "%s://%s%s" % ( self._protocol, self._address, stream_url) elif stream_url[: 1] == "/": # domain-relative (on same port) self._camera_url = "%s://%s:%d%s" % ( self._protocol, self._address, self._port, stream_url) self._camera_shares_proxy = True else: Logger.log("w", "Unusable stream url received: %s", stream_url) self._camera_url = "" Logger.log("d", "Set OctoPrint camera url to %s", self._camera_url) if self._camera_url != "" and len(self._printers) > 0: self._printers[0].setCamera( NetworkCamera(self._camera_url)) if "rotate90" in json_data["webcam"]: self._camera_rotation = -90 if json_data["webcam"][ "rotate90"] else 0 if json_data["webcam"]["flipH"] and json_data[ "webcam"]["flipV"]: self._camera_mirror = False self._camera_rotation += 180 elif json_data["webcam"]["flipH"]: self._camera_mirror = True elif json_data["webcam"]["flipV"]: self._camera_mirror = True self._camera_rotation += 180 else: self._camera_mirror = False self.cameraOrientationChanged.emit() if "plugins" in json_data: self._plugin_data = json_data["plugins"] can_update_firmware = "firmwareupdater" in self._plugin_data self._output_controller.setCanUpdateFirmware( can_update_firmware) elif reply.operation() == QNetworkAccessManager.PostOperation: if self._api_prefix + "files" in reply.url().toString( ): # Result from /files command: if http_status_code == 201: Logger.log( "d", "Resource created on OctoPrint instance: %s", reply.header( QNetworkRequest.LocationHeader).toString()) else: pass # See generic error handler below reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() if self._forced_queue or not self._auto_print: location = reply.header(QNetworkRequest.LocationHeader) if location: file_name = QUrl( reply.header(QNetworkRequest.LocationHeader). toString()).fileName() message = Message( i18n_catalog.i18nc( "@info:status", "Saved to OctoPrint as {0}").format(file_name)) else: message = Message( i18n_catalog.i18nc("@info:status", "Saved to OctoPrint")) message.addAction( "open_browser", i18n_catalog.i18nc("@action:button", "OctoPrint..."), "globe", i18n_catalog.i18nc("@info:tooltip", "Open the OctoPrint web interface")) message.actionTriggered.connect( self._onMessageActionTriggered) message.show() elif self._api_prefix + "job" in reply.url().toString( ): # Result from /job command (eg start/pause): if http_status_code == 204: Logger.log("d", "Octoprint job command accepted") else: pass # See generic error handler below elif self._api_prefix + "printer/command" in reply.url().toString( ): # Result from /printer/command (gcode statements): if http_status_code == 204: Logger.log("d", "Octoprint gcode command(s) accepted") else: pass # See generic error handler below else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) if not error_handled and http_status_code >= 400: # Received an error reply error_string = reply.attribute( QNetworkRequest.HttpReasonPhraseAttribute).decode("utf-8") if self._error_message: self._error_message.hide() self._error_message = Message( i18n_catalog.i18nc( "@info:status", "OctoPrint returned an error: {0}.").format(error_string)) self._error_message.show() return def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: # 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() progress = bytes_sent / bytes_total * 100 if progress < 100: if progress > self._progress_message.getProgress(): self._progress_message.setProgress(progress) else: self._progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Storing data on OctoPrint"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _createPrinterList(self): printer = PrinterOutputModel( output_controller=self._output_controller, number_of_extruders=self._number_of_extruders) if self._camera_url != "": printer.setCamera(NetworkCamera(self._camera_url)) printer.updateName(self.name) self._printers = [printer] self.printersChanged.emit() def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self._base_url))
class RepetierOutputDevice(PrinterOutputDevice): def __init__(self, key, address, port, properties): super().__init__(key) self._address = address self._port = port self._path = properties.get(b"path", b"/").decode("utf-8") if self._path[-1:] != "/": self._path += "/" self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None self._auto_print = True # We start with a single extruder, but update this when we get data from Repetier self._num_extruders_set = False self._num_extruders = 1 # Try to get version information from plugin.json plugin_file_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "plugin.json") try: with open(plugin_file_path) as plugin_file: plugin_info = json.load(plugin_file) plugin_version = plugin_info["version"] except: # The actual version info is not critical to have so we can continue plugin_version = "Unknown" Logger.logException( "w", "Could not get version information for the plugin") self._user_agent_header = "User-Agent".encode() self._user_agent = ( "%s/%s %s/%s" % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion(), "RepetierPlugin", Application.getInstance().getVersion())).encode() #base_url + "printer/api/" + self._key + self._api_prefix = "printer/api/" + self._key self._job_prefix = "printer/job/" + self._key self._api_header = "x-api-key".encode() self._api_key = None self._protocol = "https" if properties.get( b'useHttps') == b"true" else "http" self._base_url = "%s://%s:%d%s" % (self._protocol, self._address, self._port, self._path) self._api_url = self._base_url + self._api_prefix self._job_url = self._base_url + self._job_prefix self._basic_auth_header = "Authorization".encode() self._basic_auth_data = None basic_auth_username = properties.get(b"userName", b"").decode("utf-8") basic_auth_password = properties.get(b"password", b"").decode("utf-8") if basic_auth_username and basic_auth_password: data = base64.b64encode( ("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8") self._basic_auth_data = ("basic %s" % data).encode() self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") self.setPriority( 2 ) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print with Repetier")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print with Repetier")) self.setIconName("print") self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected to Repetier on {0}").format( self._key)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._settings_reply = None self._printer_reply = None self._job_reply = None self._command_reply = None self._image_reply = None self._stream_buffer = b"" self._stream_buffer_start_index = -1 self._post_reply = None self._post_multi_part = None self._post_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_image_id = 0 self._camera_image = QImage() self._camera_mirror = "" self._camera_rotation = 0 self._camera_url = "" self._camera_shares_proxy = False self._sd_supported = False self._connection_state_before_timeout = None self._last_response_time = None self._last_request_time = None self._response_timeout_time = 5 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._preheat_timer = QTimer() self._preheat_timer.setSingleShot(True) self._preheat_timer.timeout.connect(self.cancelPreheatBed) 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 ## Set the API key of this Repetier instance def setApiKey(self, api_key): self._api_key = api_key.encode() ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def RepetierVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address ## IP address of this instance @pyqtProperty(str, constant=True) def address(self): return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self): return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self): return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self): return self._base_url cameraOrientationChanged = pyqtSignal() @pyqtProperty("QVariantMap", notify=cameraOrientationChanged) def cameraOrientation(self): return { "mirror": self._camera_mirror, "rotation": self._camera_rotation, } def _startCamera(self): global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack or not parseBool( global_container_stack.getMetaDataEntry( "repetier_show_camera", False)) or self._camera_url == "": return # Start streaming mjpg stream url = QUrl(self._camera_url) image_request = QNetworkRequest(url) image_request.setRawHeader(self._user_agent_header, self._user_agent) if self._camera_shares_proxy and self._basic_auth_data: image_request.setRawHeader(self._basic_auth_header, self._basic_auth_data) self._image_reply = self._manager.get(image_request) self._image_reply.downloadProgress.connect( self._onStreamDownloadProgress) def _stopCamera(self): if self._image_reply: self._image_reply.abort() self._image_reply.downloadProgress.disconnect( self._onStreamDownloadProgress) self._image_reply = None image_request = None self._stream_buffer = b"" self._stream_buffer_start_index = -1 self._camera_image = QImage() self.newImage.emit() 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 # 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: self._recreate_network_manager_count += 1 Logger.log( "d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", 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.")) self._connection_message.show() # Check if we were uploading something. Abort if this is the case. # Some operating systems handle this themselves, others give weird issues. try: if self._post_reply: Logger.log( "d", "Stopping post upload because the connection was lost." ) try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._progress_message.hide() except RuntimeError: self._post_reply = None # It can happen that the wrapped c++ object is already deleted. 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 %s seconds, so it seems Repetier is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self._connection_message = Message( i18n_catalog.i18nc( "@info:status", "The connection with Repetier was lost. Check your network-connections." )) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data self._printer_reply = self._manager.get( self._createApiRequest("stateList")) ## Request print_job data self._job_reply = self._manager.get( self._createApiRequest("listPrinter")) def _createNetworkManager(self): if self._manager: self._manager.finished.disconnect(self._onRequestFinished) self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) def _createApiRequest(self, end_point): ##Logger.log("d", "Debug: %s", end_point) if "upload" in end_point: request = QNetworkRequest(QUrl(self._job_url + "?a=" + end_point)) else: request = QNetworkRequest(QUrl(self._api_url + "?a=" + end_point)) request.setRawHeader(self._user_agent_header, self._user_agent) request.setRawHeader(self._api_header, self._api_key) if self._basic_auth_data: request.setRawHeader(self._basic_auth_header, self._basic_auth_data) return request def close(self): self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() self._stopCamera() def requestWrite(self, node, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self.writeStarted.emit(self) self._gcode = getattr( Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from the instance def connect(self): self._createNetworkManager() self.setConnectionState(ConnectionState.connecting) self._update( ) # Manually trigger the first update, as we don't want to wait a few secs before it starts. Logger.log("d", "Connection with instance %s with url %s started", self._key, self._base_url) self._update_timer.start() self._last_response_time = None self.setAcceptsCommands(False) self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connecting to Repetier on {0}").format( self._base_url)) ## Request 'settings' dump self._settings_reply = self._manager.get( self._createApiRequest("getPrinterConfig")) self._settings_reply = self._manager.get( self._createApiRequest("stateList")) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %s with url %s stopped", self._key, self._base_url) 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): if job_state == "abort": command = "cancel" elif job_state == "print": if self.jobState == "paused": command = "pause" else: command = "start" elif job_state == "pause": command = "pause" if command: self._sendJobCommand(command) def startPrint(self): global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not global_container_stack: return self._auto_print = parseBool( global_container_stack.getMetaDataEntry("repetier_auto_print", True)) if self.jobState not in ["ready", ""]: if self.jobState == "offline": self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Repetier is offline. Unable to start a new job.")) elif self._auto_print: self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Repetier is busy. Unable to start a new job.")) else: # allow queueing the job even if Repetier is currently busy if autoprinting is disabled self._error_message = None if self._error_message: self._error_message.show() return self._preheat_timer.stop() if self._auto_print: Application.getInstance().showPrintMonitor.emit(True) try: self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to Repetier"), 0, False, -1) self._progress_message.addAction( "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._cancelSendGcode) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" last_process_events = time() for line in self._gcode: single_string_file_data += line if time() > last_process_events + 0.05: # Ensure that the GUI keeps updated at least 20 times per second. QCoreApplication.processEvents() last_process_events = time() job_name = Application.getInstance().getPrintInformation( ).jobName.strip() ##Logger.log("d", "debug Print job: [%s]", job_name) if job_name is "": job_name = "untitled_print" file_name = "%s.gcode" % job_name ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"a\"") self._post_part.setBody(b"upload") self._post_multi_part.append(self._post_part) ##if self._auto_print: ## self._post_part = QHttpPart() ## self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"%s\"" % file_name) ## self._post_part.setBody(b"upload") ## self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"filename\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"name\"") b = bytes(file_name, 'utf-8') self._post_part.setBody(b) self._post_multi_part.append(self._post_part) destination = "local" if self._sd_supported and parseBool( global_container_stack.getMetaDataEntry( "Repetier_store_sd", False)): destination = "sdcard" ## Post request + data #post_request = self._createApiRequest("files/" + destination) post_request = self._createApiRequest("upload") self._post_reply = self._manager.post(post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message( i18n_catalog.i18nc("@info:status", "Unable to send data to Repetier.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log( "e", "An exception occurred in network connection: %s" % str(e)) def _cancelSendGcode(self, message_id, action_id): if self._post_reply: Logger.log("d", "Stopping upload because the user pressed cancel.") try: self._post_reply.uploadProgress.disconnect( self._onUploadProgress) except TypeError: pass # The disconnection can fail on mac in some cases. Ignore that. self._post_reply.abort() self._post_reply = None self._progress_message.hide() def _sendCommand(self, command): #self._sendCommandToApi("printer/command", command) self._sendCommandToApi("send", "&data={\"cmd\":\"" + command + "\"}") Logger.log("d", "Sent gcode command to Repetier instance: %s", command) def _sendJobCommand(self, command): #self._sendCommandToApi("job", command) if (command == "pause"): if (self.jobState == "paused"): self._manager.get(self._createApiRequest("continueJob")) ##self._sendCommandToApi("send", "&data={\"cmd\":\"continueJob\"}") else: self._sendCommandToApi("send", "&data={\"cmd\":\"@pause\"}") if (command == "cancel"): self._manager.get(self._createApiRequest("stopJob")) ##self._sendCommandToApi("send", "&data={\"cmd\":\"stopJob\"}") Logger.log( "d", "Sent job command to Repetier instance: %s %s" % (command, self.jobState)) def _sendCommandToApi(self, end_point, command): command_request = self._createApiRequest(end_point) command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = "{\"command\": \"%s\"}" % command self._command_reply = self._manager.post(command_request, data.encode()) ## 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): self._setTargetBedTemperature(temperature) if duration > 0: self._preheat_timer.setInterval(duration * 1000) self._preheat_timer.start() else: self._preheat_timer.stop() ## Cancels pre-heating the heated bed of the printer. # # If the bed is not pre-heated, nothing happens. @pyqtSlot() def cancelPreheatBed(self): self._setTargetBedTemperature(0) self._preheat_timer.stop() ## Changes the target bed temperature on the Repetier instance. # # /param temperature The new target temperature of the bed. def _setTargetBedTemperature(self, temperature): if not self._updateTargetBedTemperature(temperature): Logger.log("d", "Target bed temperature is already set to %s", temperature) return Logger.log("d", "Setting bed temperature to %s", temperature) self._sendCommand("M140 S%s" % temperature) ## 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 ## Changes the target bed temperature on the Repetier instance. # # /param index The index of the hotend. # /param temperature The new target temperature of the bed. def _setTargetHotendTemperature(self, index, temperature): if not self._updateTargetHotendTemperature(index, temperature): Logger.log("d", "Target hotend %s temperature is already set to %s", index, temperature) return Logger.log("d", "Setting hotend %s temperature to %s", index, temperature) self._sendCommand("M104 T%s S%s" % (index, temperature)) ## 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 _setHeadPosition(self, x, y, z, speed): self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) def _setHeadX(self, x, speed): self._sendCommand("G0 X%s F%s" % (x, speed)) def _setHeadY(self, y, speed): self._sendCommand("G0 Y%s F%s" % (y, speed)) def _setHeadZ(self, z, speed): self._sendCommand("G0 Y%s F%s" % (z, speed)) def _homeHead(self): self._sendCommand("G28") def _homeBed(self): self._sendCommand("G28 Z") def _moveHead(self, x, y, z, speed): self._sendCommand("G91") self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) self._sendCommand("G90") ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state 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. if self._last_response_time: Logger.log( "d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return if reply.operation() == QNetworkAccessManager.GetOperation: if self._api_prefix + "?a=stateList" in reply.url().toString( ): # Status update from /printer. if http_status_code == 200: if not self.acceptsCommands: self.setAcceptsCommands(True) self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Connected to Repetier on {0}").format( self._key)) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from Repetier instance.") json_data = {} #if "temperature" in json_data: if "numExtruder" in json_data[self._key]: self._num_extruders = 0 #while "tool%d" % self._num_extruders in json_data["temperature"]: # self._num_extruders = self._num_extruders + 1 self._num_extruders = json_data[ self._key]["numExtruder"] # Reinitialise from PrinterOutputDevice to match the new _num_extruders self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [ 0 ] * self._num_extruders self._num_extruders_set = True # Check for hotend temperatures for index in range(0, self._num_extruders): if "extruder" in json_data[self._key]: hotend_temperatures = json_data[ self._key]["extruder"] self._setHotendTemperature( index, hotend_temperatures[index]["tempRead"]) self._updateTargetHotendTemperature( index, hotend_temperatures[index]["tempSet"]) else: self._setHotendTemperature(index, 0) self._updateTargetHotendTemperature(index, 0) if "heatedBed" in json_data[self._key]: bed_temperatures = json_data[ self._key]["heatedBed"] self._setBedTemperature( bed_temperatures["tempRead"]) self._updateTargetBedTemperature( bed_temperatures["tempSet"]) else: self._setBedTemperature(0) self._updateTargetBedTemperature(0) elif http_status_code == 401: self._updateJobState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "Repetier on {0} does not allow access to print"). format(self._key)) elif http_status_code == 409: if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) self._updateJobState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "The printer connected to Repetier on {0} is not operational" ).format(self._key)) else: self._updateJobState("offline") Logger.log("w", "Received an unexpected returncode: %d", http_status_code) elif self._api_prefix + "?a=listPrinter" in reply.url().toString( ): # Status update from /job: if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from Repetier instance.") json_data = {} job_state = "ready" if "job" in json_data[0]: if json_data[0]["job"] != "none": job_state = "printing" if "paused" in json_data[0]: if json_data[0]["paused"] != False: job_state = "paused" #if "state" in json_data: # if json_data["state"]["flags"]["error"]: # job_state = "error" # elif json_data["state"]["flags"]["paused"]: # job_state = "paused" # elif json_data["state"]["flags"]["printing"]: # job_state = "printing" # elif json_data["state"]["flags"]["ready"]: # job_state = "ready" self._updateJobState(job_state) #progress = json_data["progress"]["completion"] if "done" in json_data[0]: progress = json_data[0]["done"] if progress: self.setProgress(progress) if "start" in json_data[0]: if json_data[0]["start"]: ##self.setTimeElapsed(json_data[0]["start"]) ##self.setTimeElapsed(datetime.datetime.fromtimestamp(json_data[0]["start"]).strftime('%Y-%m-%d %H:%M:%S')) if json_data[0]["printedTimeComp"]: self.setTimeTotal( json_data[0]["start"] - json_data[0]["printedTimeComp"]) if json_data[0]["printTime"]: self.setTimeElapsed(json_data[0]["start"] - json_data[0]["printTime"]) elif progress > 0: self.setTimeTotal(json_data[0]["printTime"] / (progress / 100)) else: self.setTimeTotal(0) else: self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName(json_data[0]["job"]) else: pass # TODO: Handle errors elif self._api_prefix + "?a=getPrinterConfig" in reply.url( ).toString(): # Repetier settings dump from /settings: if http_status_code == 200: try: json_data = json.loads( bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid JSON from Repetier instance.") json_data = {} if "general" in json_data and "sdcard" in json_data[ "general"]: self._sd_supported = json_data["general"]["sdcard"] if "webcam" in json_data and "dynamicUrl" in json_data[ "webcam"]: self._camera_shares_proxy = False Logger.log("d", "RepetierOutputDevice: Checking streamurl") stream_url = json_data["webcam"]["dynamicUrl"].replace( "127.0.0.1", self._address) Logger.log("d", "RepetierOutputDevice: stream_url: %s", stream_url) if not stream_url: #empty string or None self._camera_url = "" elif stream_url[:4].lower() == "http": # absolute uri self._camera_url = stream_url elif stream_url[:2] == "//": # protocol-relative self._camera_url = "%s:%s" % (self._protocol, stream_url) elif stream_url[: 1] == ":": # domain-relative (on another port) self._camera_url = "%s://%s%s" % ( self._protocol, self._address, stream_url) elif stream_url[: 1] == "/": # domain-relative (on same port) self._camera_url = "%s://%s:%d%s" % ( self._protocol, self._address, self._port, stream_url) self._camera_shares_proxy = True else: Logger.log("w", "Unusable stream url received: %s", stream_url) self._camera_url = "" Logger.log("d", "Set Repetier camera url to %s", self._camera_url) self._camera_rotation = 180 self._camera_mirror = False self.cameraOrientationChanged.emit() elif reply.operation() == QNetworkAccessManager.PostOperation: if self._api_prefix + "?a=listModels" in reply.url().toString( ): # Result from /files command: if http_status_code == 201: Logger.log( "d", "Resource created on Repetier instance: %s", reply.header( QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() global_container_stack = Application.getInstance( ).getGlobalContainerStack() if not self._auto_print: location = reply.header(QNetworkRequest.LocationHeader) if location: file_name = QUrl( reply.header(QNetworkRequest.LocationHeader). toString()).fileName() message = Message( i18n_catalog.i18nc( "@info:status", "Saved to Repetier as {0}").format(file_name)) else: message = Message( i18n_catalog.i18nc("@info:status", "Saved to Repetier")) message.addAction( "open_browser", i18n_catalog.i18nc("@action:button", "Open Repetier..."), "globe", i18n_catalog.i18nc("@info:tooltip", "Open the Repetier web interface")) message.actionTriggered.connect( self._onMessageActionTriggered) message.show() elif self._api_prefix + "?a=send" in reply.url().toString( ): # Result from /job command: if http_status_code == 204: Logger.log("d", "Repetier command accepted") else: pass # TODO: Handle errors else: Logger.log("d", "RepetierOutputDevice got an unhandled operation %s", reply.operation()) def _onStreamDownloadProgress(self, bytes_received, bytes_total): self._stream_buffer += self._image_reply.readAll() 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 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: # 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() progress = bytes_sent / bytes_total * 100 if progress < 100: if progress > self._progress_message.getProgress(): self._progress_message.setProgress(progress) else: self._progress_message.hide() ##self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Storing data on Repetier"), 0, False, -1) ##self._progress_message.show() else: self._progress_message.setProgress(0) def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl(self._base_url))
class OctoPrintOutputDevice(PrinterOutputDevice): def __init__(self, key, address, properties): super().__init__(key) self._address = address self._key = key self._properties = properties # Properties dict as provided by zero conf self._gcode = None ## Todo: Hardcoded value now; we should probably read this from the machine definition and octoprint. self._num_extruders_set = False self._num_extruders = 1 self._api_version = "1" self._api_prefix = "/api/" self._api_header = "X-Api-Key" self._api_key = None self.setPriority(2) # Make sure the output device gets selected above local file output self.setName(key) self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with OctoPrint")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint")) self.setIconName("print") self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key)) # QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly # hook itself into the event loop, which results in events never being fired / done. self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onRequestFinished) ## Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to) self._printer_request = None self._printer_reply = None self._print_job_request = None self._print_job_reply = None self._image_request = None self._image_reply = None self._post_request = None self._post_reply = None self._post_multi_part = None self._post_part = None self._job_request = None self._job_reply = None self._command_request = None self._command_reply = 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._update_camera) self._camera_image_id = 0 self._camera_image = QImage() self._connection_state_before_timeout = None self._last_response_time = None self._response_timeout_time = 5 def getProperties(self): return self._properties ## 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 ## Set the API key of this OctoPrint instance def setApiKey(self, api_key): self._api_key = api_key ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant = True) def name(self): return self._key ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self): return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self): return self._address def _update_camera(self): ## Request new image url = QUrl("http://" + self._address + ":8080/?action=snapshot") self._image_request = QNetworkRequest(url) self._image_reply = self._manager.get(self._image_request) def _update(self): # Check that we aren't in a timeout state if self._last_response_time and not self._connection_state_before_timeout: if time() - self._last_response_time > self._response_timeout_time: # Go into timeout state. Logger.log("d", "We did not receive a response for %s seconds, so it seems OctoPrint is no longer accesible.", time() - self._last_response_time) self._connection_state_before_timeout = self._connection_state self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with OctoPrint was lost. Check your network-connections.")) self._connection_message.show() self.setConnectionState(ConnectionState.error) ## Request 'general' printer data url = QUrl("http://" + self._address + self._api_prefix + "printer") self._printer_request = QNetworkRequest(url) self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._printer_reply = self._manager.get(self._printer_request) ## Request print_job data url = QUrl("http://" + self._address + self._api_prefix + "job") self._job_request = QNetworkRequest(url) self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._job_reply = self._manager.get(self._job_request) def close(self): self._updateJobState("") self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() self._camera_timer.stop() self._camera_image = QImage() self.newImage.emit() def requestWrite(self, node, file_name = None, filter_by_machine = False): Application.getInstance().showPrintMonitor.emit(True) self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") self.startPrint() def isConnected(self): return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error ## Start requesting data from the instance def connect(self): self.setConnectionState(ConnectionState.connecting) self._update() # Manually trigger the first update, as we don't want to wait a few secs before it starts. self._update_camera() Logger.log("d", "Connection with instance %s with ip %s started", self._key, self._address) self._update_timer.start() self._camera_timer.start() self._last_response_time = None self.setAcceptsCommands(False) ## Stop requesting data from the instance def disconnect(self): Logger.log("d", "Connection with instance %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 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): if job_state == "abort": command = "cancel" elif job_state == "print": if self.jobState == "paused": command = "pause" else: command = "start" elif job_state == "pause": command = "pause" if command: self._sendCommand(command) def startPrint(self): global_container_stack = Application.getInstance().getGlobalContainerStack() if not global_container_stack: return if self.jobState != "ready" and self.jobState != "": self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is printing. Unable to start a new job.")) self._error_message.show() return try: self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1) self._progress_message.show() ## Mash the data into single string single_string_file_data = "" for line in self._gcode: single_string_file_data += line file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName ## Create multi_part request self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType) ## Create parts (to be placed inside multipart) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) if global_container_stack.getMetaDataEntry("octoprint_auto_print", True): self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"") self._post_part.setBody(b"true") self._post_multi_part.append(self._post_part) self._post_part = QHttpPart() self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) self._post_part.setBody(single_string_file_data.encode()) self._post_multi_part.append(self._post_part) url = QUrl("http://" + self._address + self._api_prefix + "files/local") ## Create the QT request self._post_request = QNetworkRequest(url) self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) ## Post request + data self._post_reply = self._manager.post(self._post_request, self._post_multi_part) self._post_reply.uploadProgress.connect(self._onUploadProgress) self._gcode = None except IOError: self._progress_message.hide() self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint.")) self._error_message.show() except Exception as e: self._progress_message.hide() Logger.log("e", "An exception occurred in network connection: %s" % str(e)) def _sendCommand(self, command): url = QUrl("http://" + self._address + self._api_prefix + "job") self._command_request = QNetworkRequest(url) self._command_request.setRawHeader(self._api_header.encode(), self._api_key.encode()) self._command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") data = "{\"command\": \"%s\"}" % command self._command_reply = self._manager.post(self._command_request, data.encode()) Logger.log("d", "Sent command to OctoPrint instance: %s", data) def _setTargetBedTemperature(self, temperature): Logger.log("d", "Setting bed temperature to %s", temperature) self._sendCommand("M140 S%s" % temperature) def _setTargetHotendTemperature(self, index, temperature): Logger.log("d", "Setting hotend %s temperature to %s", index, temperature) self._sendCommand("M104 T%s S%s" % (index, temperature)) def _setHeadPosition(self, x, y , z, speed): self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) def _setHeadX(self, x, speed): self._sendCommand("G0 X%s F%s" % (x, speed)) def _setHeadY(self, y, speed): self._sendCommand("G0 Y%s F%s" % (y, speed)) def _setHeadZ(self, z, speed): self._sendCommand("G0 Y%s F%s" % (z, speed)) def _homeHead(self): self._sendCommand("G28") def _homeBed(self): self._sendCommand("G28 Z") def _moveHead(self, x, y, z, speed): self._sendCommand("G91") self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) self._sendCommand("G90") ## Handler for all requests that have finished. def _onRequestFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the instance") self._connection_state_before_timeout = self._connection_state 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. if self._last_response_time: Logger.log("d", "We got a response from the instance after %s of silence", time() - self._last_response_time) self.setConnectionState(self._connection_state_before_timeout) self._connection_state_before_timeout = None if reply.error() == QNetworkReply.NoError: self._last_response_time = time() http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not http_status_code: # Received no or empty reply return if reply.operation() == QNetworkAccessManager.GetOperation: if "printer" in reply.url().toString(): # Status update from /printer. if http_status_code == 200: self.setAcceptsCommands(True) if self._connection_state == ConnectionState.connecting: self.setConnectionState(ConnectionState.connected) json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) if not self._num_extruders_set: self._num_extruders = 0 while "tool%d" % self._num_extruders in json_data["temperature"]: self._num_extruders = self._num_extruders + 1 # Reinitialise from PrinterOutputDevice to match the new _num_extruders self._hotend_temperatures = [0] * self._num_extruders self._target_hotend_temperatures = [0] * self._num_extruders self._num_extruders_set = True # Check for hotend temperatures for index in range(0, self._num_extruders): temperature = json_data["temperature"]["tool%d" % index]["actual"] self._setHotendTemperature(index, temperature) bed_temperature = json_data["temperature"]["bed"]["actual"] self._setBedTemperature(bed_temperature) job_state = "offline" if json_data["state"]["flags"]["error"]: job_state = "error" elif json_data["state"]["flags"]["paused"]: job_state = "paused" elif json_data["state"]["flags"]["printing"]: job_state = "printing" elif json_data["state"]["flags"]["ready"]: job_state = "ready" self._updateJobState(job_state) elif http_status_code == 401: self.setAcceptsCommands(False) else: pass # TODO: Handle errors elif "job" in reply.url().toString(): # Status update from /job: if http_status_code == 200: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) progress = json_data["progress"]["completion"] if progress: self.setProgress(progress) if json_data["progress"]["printTime"]: self.setTimeElapsed(json_data["progress"]["printTime"]) if json_data["progress"]["printTimeLeft"]: self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"]) elif json_data["job"]["estimatedPrintTime"]: self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"])) elif progress > 0: self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100)) else: self.setTimeTotal(0) else: self.setTimeElapsed(0) self.setTimeTotal(0) self.setJobName(json_data["job"]["file"]["name"]) else: pass # TODO: Handle errors elif "snapshot" in reply.url().toString(): # Update from camera: if http_status_code == 200: self._camera_image.loadFromData(reply.readAll()) self.newImage.emit() else: pass # TODO: Handle errors elif reply.operation() == QNetworkAccessManager.PostOperation: if "files" in reply.url().toString(): # Result from /files command: if http_status_code == 201: Logger.log("d", "Resource created on OctoPrint instance: %s", reply.header(QNetworkRequest.LocationHeader).toString()) else: pass # TODO: Handle errors reply.uploadProgress.disconnect(self._onUploadProgress) self._progress_message.hide() global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack and not global_container_stack.getMetaDataEntry("octoprint_auto_print", True): message = Message(catalog.i18nc("@info:status", "Saved to OctoPrint as {1}").format(reply.header(QNetworkRequest.LocationHeader).toString())) message.addAction("open_browser", catalog.i18nc("@action:button", "Open Browser"), "globe", catalog.i18nc("@info:tooltip", "Open browser to OctoPrint.")) message.actionTriggered.connect(self._onMessageActionTriggered) message.show() elif "job" in reply.url().toString(): # Result from /job command: if http_status_code == 204: Logger.log("d", "Octoprint command accepted") else: pass # TODO: Handle errors else: Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation()) def _onUploadProgress(self, bytes_sent, bytes_total): if bytes_total > 0: progress = bytes_sent / bytes_total * 100 if progress < 100: self._progress_message.setProgress(progress) else: self._progress_message.hide() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Storing data on OctoPrint"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl(QUrl("http://" + self._address))