class ProcessSlicedObjectListJob(Job): def __init__(self, message): super().__init__() self._message = message self._scene = Application.getInstance().getController().getScene() self._progress = None self._abort_requested = False ## Aborts the processing of layers. # # This abort is made on a best-effort basis, meaning that the actual # job thread will check once in a while to see whether an abort is # requested and then stop processing by itself. There is no guarantee # that the abort will stop the job any time soon or even at all. def abort(self): self._abort_requested = True def run(self): if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) object_id_map = {} new_node = SceneNode() ## Put all nodes in a dictionary identified by ID for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData(): if node.callDecoration("getLayerData"): self._scene.getRoot().removeChild(node) else: object_id_map[id(node)] = node Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return settings = Application.getInstance().getMachineManager().getWorkingProfile() mesh = MeshData() layer_data = LayerData.LayerData() layer_count = 0 for i in range(self._message.repeatedMessageCount("objects")): layer_count += self._message.getRepeatedMessage("objects", i).repeatedMessageCount("layers") current_layer = 0 for object_position in range(self._message.repeatedMessageCount("objects")): current_object = self._message.getRepeatedMessage("objects", object_position) try: node = object_id_map[current_object.id] except KeyError: continue for l in range(current_object.repeatedMessageCount("layers")): layer = current_object.getRepeatedMessage("layers", l) layer_data.addLayer(layer.id) layer_data.setLayerHeight(layer.id, layer.height) layer_data.setLayerThickness(layer.id, layer.thickness) for p in range(layer.repeatedMessageCount("polygons")): polygon = layer.getRepeatedMessage("polygons", p) points = numpy.fromstring(polygon.points, dtype="i8") # Convert bytearray to numpy array points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) new_points[:,0] = points[:,0] new_points[:,1] = layer.height new_points[:,2] = -points[:,1] new_points /= 1000 layer_data.addPolygon(layer.id, polygon.type, new_points, polygon.line_width) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 100 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress: self._progress.hide() return if self._progress: self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data layer_data.build() if self._abort_requested: if self._progress: self._progress.hide() return #Add layerdata decorator to scene node to indicate that the node has layerdata decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_data) new_node.addDecorator(decorator) new_node.setMeshData(mesh) new_node.setParent(self._scene.getRoot()) #Note: After this we can no longer abort! if not settings.getSettingValue("machine_center_is_zero"): new_node.setPosition(Vector(-settings.getSettingValue("machine_width") / 2, 0.0, settings.getSettingValue("machine_depth") / 2)) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide() def _onActiveViewChanged(self): if self.isRunning(): if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": if not self._progress: self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0) if self._progress.getProgress() != 100: self._progress.show() else: if self._progress: self._progress.hide()
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 ProcessSVGJob(Job): def __init__(self, polyLine,hole_polyLine): super().__init__() self._polyLine = polyLine self._hole_polyLine = hole_polyLine #self._layers = layers Logger.log('e',"SB----SB") print (polyLine,hole_polyLine) # self._scene = Application.getInstance().getController().getScene() #self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._abort_requested = False self._build_plate_number = None self._isTri = False #是否可以获得数据 self._triangles = None ## 终止层的处理。 # # This abort is made on a best-effort basis, meaning that the actual # job thread will check once in a while to see whether an abort is # requested and then stop processing by itself. There is no guarantee # that the abort will stop the job any time soon or even at all. def abort(self): self._abort_requested = True def setPolyLine(self, new_value): self._polyLine = new_value def getPolyLine(self): return self._polyLine def setHole_polyLine(self, new_value): self._hole_polyLine = new_value def getHole_polyLine(self): return self._hole_polyLine def getTriangles(self): if self._isTri: return self._triangles else: return None def run(self): # Logger.log('e', polyLine + hole_polyLine) start_time = time() cdt = CDT(self._polyLine) if hole_polyLine: cdt.add_hole(self._hole_polyLine) self._triangles = cdt.triangulate() self._isTri = True # fig = plt.figure() # ax = Axes3D(fig) # for t in triangles: # p0 = [t.a.x, t.a.y, t.a.z] # p1 = [t.b.x, t.b.y, t.b.z] # p2 = [t.c.x, t.c.y, t.c.z] # x = [t.a.x, t.b.x, t.c.x, t.a.x] # y = [t.a.y, t.b.y, t.c.y, t.a.y] # z = [t.a.z, t.b.z, t.c.z, t.a.z] # # 绘制线型图 # ax.plot(x, y, z) # # # 显示图 # plt.show() #return triangles #Logger.log("d", "Processing new layer for build plate %s..." % self._build_plate_number) # view = Application.getInstance().getController().getActiveView() # if view.getPluginId() == "SimulationView": # view.resetLayerData() # self._progress_message.show() # Job.yieldThread() # if self._abort_requested: # if self._progress_message: # self._progress_message.hide() # return # # Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) # # # The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice # new_node = CuraSceneNode(no_setting_override = True) # new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) # # # Force garbage collection. # # For some reason, Python has a tendency to keep the layer data # # in memory longer than needed. Forcing the GC to run here makes # # sure any old layer data is really cleaned up before adding new. # gc.collect() # # mesh = MeshData() # layer_data = LayerDataBuilder.LayerDataBuilder() # layer_count = len(self._layers) # # # Find the minimum layer number # # When disabling the remove empty first layers setting, the minimum layer number will be a positive # # value. In that case the first empty layers will be discarded and start processing layers from the # # first layer with data. # # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # # simply offset all other layers so the lowest layer is always 0. It could happens that the first # # raft layer has value -8 but there are just 4 raft (negative) layers. # min_layer_number = sys.maxsize # negative_layers = 0 # for layer in self._layers: # if layer.repeatedMessageCount("path_segment") > 0: # if layer.id < min_layer_number: # min_layer_number = layer.id # if layer.id < 0: # negative_layers += 1 # # current_layer = 0 # # for layer in self._layers: # # If the layer is below the minimum, it means that there is no data, so that we don't create a layer # # data. However, if there are empty layers in between, we compute them. # if layer.id < min_layer_number: # continue # # # Layers are offset by the minimum layer number. In case the raft (negative layers) is being used, # # then the absolute layer number is adjusted by removing the empty layers that can be in between raft # # and the model # abs_layer_number = layer.id - min_layer_number # if layer.id >= 0 and negative_layers != 0: # abs_layer_number += (min_layer_number + negative_layers) # # layer_data.addLayer(abs_layer_number) # this_layer = layer_data.getLayer(abs_layer_number) # layer_data.setLayerHeight(abs_layer_number, layer.height) # layer_data.setLayerThickness(abs_layer_number, layer.thickness) # # for p in range(layer.repeatedMessageCount("path_segment")): # polygon = layer.getRepeatedMessage("path_segment", p) # # extruder = polygon.extruder # # line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array # line_types = line_types.reshape((-1,1)) # # points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array # if polygon.point_type == 0: # Point2D # points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # else: # Point3D # points = points.reshape((-1,3)) # # line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array # line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # # line_thicknesses = numpy.fromstring(polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array # line_thicknesses = line_thicknesses.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # # line_feedrates = numpy.fromstring(polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array # line_feedrates = line_feedrates.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # # # Create a new 3D-array, copy the 2D points over and insert the right height. # # This uses manual array creation + copy rather than numpy.insert since this is # # faster. # new_points = numpy.empty((len(points), 3), numpy.float32) # if polygon.point_type == 0: # Point2D # new_points[:, 0] = points[:, 0] # new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation # new_points[:, 2] = -points[:, 1] # else: # Point3D # new_points[:, 0] = points[:, 0] # new_points[:, 1] = points[:, 2] # new_points[:, 2] = -points[:, 1] # # this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates) # this_poly.buildCache() # # this_layer.polygons.append(this_poly) # # Job.yieldThread() # Job.yieldThread() # current_layer += 1 # progress = (current_layer / layer_count) * 99 # # TODO: Rebuild the layer data mesh once the layer has been processed. # # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. # # if self._abort_requested: # if self._progress_message: # self._progress_message.hide() # return # if self._progress_message: # self._progress_message.setProgress(progress) # # # We are done processing all the layers we got from the engine, now create a mesh out of the data # # # Find out colors per extruder # global_container_stack = Application.getInstance().getGlobalContainerStack() # manager = ExtruderManager.getInstance() # extruders = manager.getActiveExtruderStacks() # if extruders: # material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) # for extruder in extruders: # position = int(extruder.getMetaDataEntry("position", default="0")) # Get the position # try: # default_color = ExtrudersModel.defaultColors[position] # except IndexError: # default_color = "#e0e000" # color_code = extruder.material.getMetaDataEntry("color_code", default=default_color) # color = colorCodeToRGBA(color_code) # material_color_map[position, :] = color # else: # # Single extruder via global stack. # material_color_map = numpy.zeros((1, 4), dtype=numpy.float32) # color_code = global_container_stack.material.getMetaDataEntry("color_code", default="#e0e000") # color = colorCodeToRGBA(color_code) # material_color_map[0, :] = color # # # We have to scale the colors for compatibility mode # if OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode")): # line_type_brightness = 0.5 # for compatibility mode # else: # line_type_brightness = 1.0 # layer_mesh = layer_data.build(material_color_map, line_type_brightness) # # if self._abort_requested: # if self._progress_message: # self._progress_message.hide() # return # # # Add LayerDataDecorator to scene node to indicate that the node has layer data # decorator = LayerDataDecorator.LayerDataDecorator() # decorator.setLayerData(layer_mesh) # new_node.addDecorator(decorator) # # new_node.setMeshData(mesh) # # Set build volume as parent, the build volume can move as a result of raft settings. # # It makes sense to set the build volume as parent: the print is actually printed on it. # new_node_parent = Application.getInstance().getBuildVolume() # new_node.setParent(new_node_parent) # Note: After this we can no longer abort! # # settings = Application.getInstance().getGlobalContainerStack() # if not settings.getProperty("machine_center_is_zero", "value"): # new_node.setPosition(Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) # # if self._progress_message: # self._progress_message.setProgress(100) # # if self._progress_message: # self._progress_message.hide() # # # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. # self._layers = None # Logger.log("d", "Processing layers took %s seconds", time() - start_time) def _onActiveViewChanged(self): if self.isRunning(): if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView": if not self._progress_message: self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0, catalog.i18nc("@info:title", "Information")) if self._progress_message.getProgress() != 100: self._progress_message.show() else: if self._progress_message: self._progress_message.hide()
class ProcessSlicedLayersJob(Job): def __init__(self, layers): super().__init__() self._layers = layers self._scene = Application.getInstance().getController().getScene() self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._abort_requested = False self._build_plate_number = None ## Aborts the processing of layers. # # This abort is made on a best-effort basis, meaning that the actual # job thread will check once in a while to see whether an abort is # requested and then stop processing by itself. There is no guarantee # that the abort will stop the job any time soon or even at all. def abort(self): self._abort_requested = True def setBuildPlate(self, new_value): self._build_plate_number = new_value def getBuildPlate(self): return self._build_plate_number def run(self): Logger.log("d", "Processing new layer for build plate %s..." % self._build_plate_number) start_time = time() view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "SimulationView": view.resetLayerData() self._progress_message.show() Job.yieldThread() if self._abort_requested: if self._progress_message: self._progress_message.hide() return Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) # The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice new_node = CuraSceneNode(no_setting_override = True) new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) # Force garbage collection. # For some reason, Python has a tendency to keep the layer data # in memory longer than needed. Forcing the GC to run here makes # sure any old layer data is really cleaned up before adding new. gc.collect() mesh = MeshData() layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number # When disabling the remove empty first layers setting, the minimum layer number will be a positive # value. In that case the first empty layers will be discarded and start processing layers from the # first layer with data. # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # simply offset all other layers so the lowest layer is always 0. It could happens that the first # raft layer has value -8 but there are just 4 raft (negative) layers. min_layer_number = sys.maxsize negative_layers = 0 for layer in self._layers: if layer.repeatedMessageCount("path_segment") > 0: if layer.id < min_layer_number: min_layer_number = layer.id if layer.id < 0: negative_layers += 1 current_layer = 0 for layer in self._layers: # If the layer is below the minimum, it means that there is no data, so that we don't create a layer # data. However, if there are empty layers in between, we compute them. if layer.id < min_layer_number: continue # Layers are offset by the minimum layer number. In case the raft (negative layers) is being used, # then the absolute layer number is adjusted by removing the empty layers that can be in between raft # and the model abs_layer_number = layer.id - min_layer_number if layer.id >= 0 and negative_layers != 0: abs_layer_number += (min_layer_number + negative_layers) layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) layer_data.setLayerThickness(abs_layer_number, layer.thickness) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) extruder = polygon.extruder line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = line_types.reshape((-1,1)) points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array if polygon.point_type == 0: # Point2D points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: # Point3D points = points.reshape((-1,3)) line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_thicknesses = numpy.fromstring(polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array line_thicknesses = line_thicknesses.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_feedrates = numpy.fromstring(polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array line_feedrates = line_feedrates.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) if polygon.point_type == 0: # Point2D new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation new_points[:, 2] = -points[:, 1] else: # Point3D new_points[:, 0] = points[:, 0] new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates) this_poly.buildCache() this_layer.polygons.append(this_poly) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 99 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress_message: self._progress_message.hide() return if self._progress_message: self._progress_message.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data # Find out colors per extruder global_container_stack = Application.getInstance().getGlobalContainerStack() manager = ExtruderManager.getInstance() extruders = manager.getActiveExtruderStacks() if extruders: material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) for extruder in extruders: position = int(extruder.getMetaDataEntry("position", default = "0")) try: default_color = ExtrudersModel.defaultColors[position] except IndexError: default_color = "#e0e000" color_code = extruder.material.getMetaDataEntry("color_code", default=default_color) color = colorCodeToRGBA(color_code) material_color_map[position, :] = color else: # Single extruder via global stack. material_color_map = numpy.zeros((1, 4), dtype=numpy.float32) color_code = global_container_stack.material.getMetaDataEntry("color_code", default="#e0e000") color = colorCodeToRGBA(color_code) material_color_map[0, :] = color # We have to scale the colors for compatibility mode if OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode")): line_type_brightness = 0.5 # for compatibility mode else: line_type_brightness = 1.0 layer_mesh = layer_data.build(material_color_map, line_type_brightness) if self._abort_requested: if self._progress_message: self._progress_message.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) # Set build volume as parent, the build volume can move as a result of raft settings. # It makes sense to set the build volume as parent: the print is actually printed on it. new_node_parent = Application.getInstance().getBuildVolume() new_node.setParent(new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition(Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) if self._progress_message: self._progress_message.setProgress(100) if self._progress_message: self._progress_message.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None Logger.log("d", "Processing layers took %s seconds", time() - start_time) def _onActiveViewChanged(self): if self.isRunning(): if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView": if not self._progress_message: self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0, catalog.i18nc("@info:title", "Information")) if self._progress_message.getProgress() != 100: self._progress_message.show() else: if self._progress_message: self._progress_message.hide()
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))
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): printJobsChanged = pyqtSignal() activePrinterChanged = pyqtSignal() # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in. # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions. clusterPrintersChanged = pyqtSignal() def __init__(self, device_id, address, properties, parent = None): super().__init__(device_id = device_id, address = address, properties=properties, parent = parent) self._api_prefix = "/cluster-api/v1/" self._number_of_extruders = 2 self._dummy_lambdas = set() self._print_jobs = [] 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") # See comments about this hack with the clusterPrintersChanged signal self.printersChanged.connect(self.clusterPrintersChanged) self._accepts_commands = True # Cluster does not have authentication, so default to authenticated self._authentication_state = AuthState.Authenticated self._error_message = None self._write_job_progress_message = None self._progress_message = None self._active_printer = None # type: Optional[PrinterOutputModel] self._printer_selection_dialog = None self.setPriority(3) # Make sure the output device gets selected above local file output self.setName(self._id) self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network")) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network")) self._printer_uuid_to_unique_name_mapping = {} self._finished_jobs = [] self._cluster_size = int(properties.get(b"cluster_size", 0)) self._latest_reply_handler = None def requestWrite(self, nodes: List[SceneNode], file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self.writeStarted.emit(self) #Formats supported by this application (file types that we can actually write). if file_handler: file_formats = file_handler.getSupportedFileTypesWrite() else: file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() #Create a list from the supported file formats string. machine_file_formats = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("file_formats").split(";") machine_file_formats = [file_type.strip() for file_type in machine_file_formats] #Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format. if "application/x-ufp" not in machine_file_formats and self.printerType == "ultimaker3" and Version(self.firmwareVersion) >= Version("4.4"): machine_file_formats = ["application/x-ufp"] + machine_file_formats # Take the intersection between file_formats and machine_file_formats. format_by_mimetype = {format["mime_type"]: format for format in file_formats} file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats. if len(file_formats) == 0: Logger.log("e", "There are no file formats available to write with!") raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!")) preferred_format = file_formats[0] #Just take the first file format available. if file_handler is not None: writer = file_handler.getWriterByMimeType(preferred_format["mime_type"]) else: writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(preferred_format["mime_type"]) #This function pauses with the yield, waiting on instructions on which printer it needs to print with. self._sending_job = self._sendPrintJob(writer, preferred_format, nodes) self._sending_job.send(None) #Start the generator. if len(self._printers) > 1: #We need to ask the user. self._spawnPrinterSelectionDialog() is_job_sent = True else: #Just immediately continue. self._sending_job.send("") #No specifically selected printer. is_job_sent = self._sending_job.send(None) def _spawnPrinterSelectionDialog(self): if self._printer_selection_dialog is None: path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml") self._printer_selection_dialog = Application.getInstance().createQmlComponent(path, {"OutputDevice": self}) if self._printer_selection_dialog is not None: self._printer_selection_dialog.show() @pyqtProperty(int, constant=True) def clusterSize(self): return self._cluster_size ## Allows the user to choose a printer to print with from the printer # selection dialogue. # \param target_printer The name of the printer to target. @pyqtSlot(str) def selectPrinter(self, target_printer: str = "") -> None: self._sending_job.send(target_printer) ## Greenlet to send a job to the printer over the network. # # This greenlet gets called asynchronously in requestWrite. It is a # greenlet in order to optionally wait for selectPrinter() to select a # printer. # The greenlet yields exactly three times: First time None, # \param writer The file writer to use to create the data. # \param preferred_format A dictionary containing some information about # what format to write to. This is necessary to create the correct buffer # types and file extension and such. def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict, nodes: List[SceneNode]): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: self._error_message = Message( i18n_catalog.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job.")) self._error_message.show() yield #Wait on the user to select a target printer. yield #Wait for the write job to be finished. yield False #Return whether this was a success or not. yield #Prevent StopIteration. self._sending_gcode = True target_printer = yield #Potentially wait on the user to select a target printer. # Using buffering greatly reduces the write time for many lines of gcode if preferred_format["mode"] == FileWriter.OutputMode.TextMode: stream = io.StringIO() else: #Binary mode. stream = io.BytesIO() job = WriteFileJob(writer, stream, nodes, preferred_format["mode"]) self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1, title = i18n_catalog.i18nc("@info:title", "Sending Data"), use_inactivity_timer = False) self._write_job_progress_message.show() self._dummy_lambdas = (target_printer, preferred_format, stream) job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished) job.start() yield True #Return that we had success! yield #To prevent having to catch the StopIteration exception. from cura.Utils.Threading import call_on_qt_thread def _sendPrintJobWaitOnWriteJobFinished(self, job): self._write_job_progress_message.hide() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1, title = i18n_catalog.i18nc("@info:title", "Sending Data")) self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = None, description = "") self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered) self._progress_message.show() parts = [] target_printer, preferred_format, stream = self._dummy_lambdas # If a specific printer was selected, it should be printed with that machine. if target_printer: target_printer = self._printer_uuid_to_unique_name_mapping[target_printer] parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain")) # Add user name to the print_job parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) file_name = Application.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"] output = stream.getvalue() #Either str or bytes depending on the output mode. if isinstance(stream, io.StringIO): output = output.encode("utf-8") parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output)) self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress) @pyqtProperty(QObject, notify=activePrinterChanged) def activePrinter(self) -> Optional[PrinterOutputModel]: return self._active_printer @pyqtSlot(QObject) def setActivePrinter(self, printer: Optional[PrinterOutputModel]): if self._active_printer != printer: if self._active_printer and self._active_printer.camera: self._active_printer.camera.stop() self._active_printer = printer self.activePrinterChanged.emit() def _onPostPrintJobFinished(self, reply): self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False def _onUploadPrintJobProgress(self, bytes_sent:int, bytes_total:int): if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() if new_progress > self._progress_message.getProgress(): self._progress_message.show() # Ensure that the message is visible. self._progress_message.setProgress(bytes_sent / bytes_total * 100) # If successfully sent: if bytes_sent == bytes_total: # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to the # monitor tab. self._success_message = Message( i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."), lifetime=5, dismissable=True, title=i18n_catalog.i18nc("@info:title", "Data Sent")) self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Monitor"), icon=None, description="") self._success_message.actionTriggered.connect(self._successMessageActionTriggered) self._success_message.show() else: self._progress_message.setProgress(0) self._progress_message.hide() def _progressMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None: if action_id == "Abort": Logger.log("d", "User aborted sending print to remote.") self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False Application.getInstance().getController().setActiveStage("PrepareStage") # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request # the "reply" should be disconnected if self._latest_reply_handler: self._latest_reply_handler.disconnect() self._latest_reply_handler = None def _successMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None: if action_id == "View": Application.getInstance().getController().setActiveStage("MonitorStage") @pyqtSlot() def openPrintJobControlPanel(self) -> None: Logger.log("d", "Opening print job control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs")) @pyqtSlot() def openPrinterControlPanel(self) -> None: Logger.log("d", "Opening printer control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers")) @pyqtProperty("QVariantList", notify=printJobsChanged) def printJobs(self)-> List[PrintJobOutputModel] : return self._print_jobs @pyqtProperty("QVariantList", notify=printJobsChanged) def queuedPrintJobs(self) -> List[PrintJobOutputModel]: return [print_job for print_job in self._print_jobs if print_job.state == "queued"] @pyqtProperty("QVariantList", notify=printJobsChanged) def activePrintJobs(self) -> List[PrintJobOutputModel]: return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"] @pyqtProperty("QVariantList", notify=clusterPrintersChanged) def connectedPrintersTypeCount(self) -> List[PrinterOutputModel]: printer_count = {} for printer in self._printers: if printer.type in printer_count: printer_count[printer.type] += 1 else: printer_count[printer.type] = 1 result = [] for machine_type in printer_count: result.append({"machine_type": machine_type, "count": printer_count[machine_type]}) return result @pyqtSlot(int, result=str) def formatDuration(self, seconds: int) -> str: return Duration(seconds).getDisplayString(DurationFormat.Format.Short) @pyqtSlot(int, result=str) def getTimeCompleted(self, time_remaining: int) -> str: current_time = time() datetime_completed = 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: int) -> str: current_time = time() datetime_completed = datetime.fromtimestamp(current_time + time_remaining) return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper() def _printJobStateChanged(self) -> None: username = self._getUserName() if username is None: return # We only want to show notifications if username is set. finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"] newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username] for job in newly_finished_jobs: if job.assignedPrinter: job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name)) else: job_completed_text = i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.".format(job_name = job.name)) job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished")) job_completed_message.show() # Ensure UI gets updated self.printJobsChanged.emit() # Keep a list of all completed jobs so we know if something changed next time. self._finished_jobs = finished_jobs def _update(self) -> None: if not super()._update(): return self.get("printers/", onFinished=self._onGetPrintersDataFinished) self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished) def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None: if not checkValidGetReply(reply): return result = loadJsonFromReply(reply) if result is None: return print_jobs_seen = [] job_list_changed = False for print_job_data in result: print_job = findByKey(self._print_jobs, print_job_data["uuid"]) if print_job is None: print_job = self._createPrintJobModel(print_job_data) job_list_changed = True self._updatePrintJob(print_job, print_job_data) if print_job.state != "queued": # Print job should be assigned to a printer. if print_job.state in ["failed", "finished", "aborted"]: # Print job was already completed, so don't attach it to a printer. printer = None else: printer = self._getPrinterByKey(print_job_data["printer_uuid"]) else: # The job can "reserve" a printer if some changes are required. printer = self._getPrinterByKey(print_job_data["assigned_to"]) if printer: printer.updateActivePrintJob(print_job) print_jobs_seen.append(print_job) # Check what jobs need to be removed. removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen] for removed_job in removed_jobs: job_list_changed |= self._removeJob(removed_job) if job_list_changed: self.printJobsChanged.emit() # Do a single emit for all print job changes. def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None: if not checkValidGetReply(reply): return result = loadJsonFromReply(reply) if result is None: return printer_list_changed = False printers_seen = [] for printer_data in result: printer = findByKey(self._printers, printer_data["uuid"]) if printer is None: printer = self._createPrinterModel(printer_data) printer_list_changed = True printers_seen.append(printer) self._updatePrinter(printer, printer_data) removed_printers = [printer for printer in self._printers if printer not in printers_seen] for printer in removed_printers: self._removePrinter(printer) if removed_printers or printer_list_changed: self.printersChanged.emit() def _createPrinterModel(self, data: Dict) -> PrinterOutputModel: printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self), number_of_extruders=self._number_of_extruders) printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream")) self._printers.append(printer) return printer def _createPrintJobModel(self, data: Dict) -> PrintJobOutputModel: print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self), key=data["uuid"], name= data["name"]) print_job.stateChanged.connect(self._printJobStateChanged) self._print_jobs.append(print_job) return print_job def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict) -> None: print_job.updateTimeTotal(data["time_total"]) print_job.updateTimeElapsed(data["time_elapsed"]) print_job.updateState(data["status"]) print_job.updateOwner(data["owner"]) def _updatePrinter(self, printer: PrinterOutputModel, data: Dict) -> None: # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer. # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping. self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"] definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"]) if not definitions: Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"]) return machine_definition = definitions[0] printer.updateName(data["friendly_name"]) printer.updateKey(data["uuid"]) printer.updateType(data["machine_variant"]) # Do not store the buildplate information that comes from connect if the current printer has not buildplate information if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False): printer.updateBuildplateName(data["build_plate"]["type"]) if not data["enabled"]: printer.updateState("disabled") else: printer.updateState(data["status"]) for index in range(0, self._number_of_extruders): extruder = printer.extruders[index] try: extruder_data = data["configuration"][index] except IndexError: break extruder.updateHotendID(extruder_data.get("print_core_id", "")) material_data = extruder_data["material"] if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]: containers = ContainerRegistry.getInstance().findInstanceContainers(type="material", GUID=material_data["guid"]) if containers: color = containers[0].getMetaDataEntry("color_code") brand = containers[0].getMetaDataEntry("brand") material_type = containers[0].getMetaDataEntry("material") name = containers[0].getName() else: Logger.log("w", "Unable to find material with guid {guid}. Using data as provided by cluster".format( guid=material_data["guid"])) color = material_data["color"] brand = material_data["brand"] material_type = material_data["material"] name = "Empty" if material_data["material"] == "empty" else "Unknown" material = MaterialOutputModel(guid=material_data["guid"], type=material_type, brand=brand, color=color, name=name) extruder.updateActiveMaterial(material) def _removeJob(self, job: PrintJobOutputModel): if job not in self._print_jobs: return False if job.assignedPrinter: job.assignedPrinter.updateActivePrintJob(None) job.stateChanged.disconnect(self._printJobStateChanged) self._print_jobs.remove(job) return True def _removePrinter(self, printer: PrinterOutputModel): self._printers.remove(printer) if self._active_printer == printer: self._active_printer = None self.activePrinterChanged.emit()
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): def __init__(self, device_id, address: str, properties, parent = None): super().__init__(device_id = device_id, address = address, properties = properties, parent = parent) self._api_prefix = "/api/v1/" self._number_of_extruders = 2 self._authentication_id = None self._authentication_key = None self._authentication_counter = 0 self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min) self._authentication_timer = QTimer() self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval self._authentication_timer.setSingleShot(False) self._authentication_timer.timeout.connect(self._onAuthenticationTimer) # The messages are created when connect is called the first time. # This ensures that the messages are only created for devices that actually want to connect. self._authentication_requested_message = None self._authentication_failed_message = None self._authentication_succeeded_message = None self._not_authenticated_message = None self.authenticationStateChanged.connect(self._onAuthenticationStateChanged) self.setPriority(3) # Make sure the output device gets selected above local file output self.setName(self._id) self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network")) self.setIconName("print") self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") self._output_controller = LegacyUM3PrinterOutputController(self) def _onAuthenticationStateChanged(self): # We only accept commands if we are authenticated. self._setAcceptsCommands(self._authentication_state == AuthState.Authenticated) if self._authentication_state == AuthState.Authenticated: self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network.")) elif self._authentication_state == AuthState.AuthenticationRequested: self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. Please approve the access request on the printer.")) elif self._authentication_state == AuthState.AuthenticationDenied: self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. No access to control the printer.")) def _setupMessages(self): self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Access to the printer requested. Please approve the request on the printer"), lifetime=0, dismissable=False, progress=0, title=i18n_catalog.i18nc("@info:title", "Authentication status")) self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", ""), title=i18n_catalog.i18nc("@info:title", "Authentication Status")) self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the access request")) self._authentication_failed_message.actionTriggered.connect(self._messageCallback) self._authentication_succeeded_message = Message( i18n_catalog.i18nc("@info:status", "Access to the printer accepted"), title=i18n_catalog.i18nc("@info:title", "Authentication Status")) self._not_authenticated_message = Message( i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."), title=i18n_catalog.i18nc("@info:title", "Authentication Status")) self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"), None, i18n_catalog.i18nc("@info:tooltip", "Send access request to the printer")) self._not_authenticated_message.actionTriggered.connect(self._messageCallback) def _messageCallback(self, message_id=None, action_id="Retry"): if action_id == "Request" or action_id == "Retry": if self._authentication_failed_message: self._authentication_failed_message.hide() if self._not_authenticated_message: self._not_authenticated_message.hide() self._requestAuthentication() def connect(self): super().connect() self._setupMessages() global_container = Application.getInstance().getGlobalContainerStack() if global_container: self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None) self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None) def close(self): super().close() if self._authentication_requested_message: self._authentication_requested_message.hide() if self._authentication_failed_message: self._authentication_failed_message.hide() if self._authentication_succeeded_message: self._authentication_succeeded_message.hide() self._sending_gcode = False self._compressing_gcode = False self._authentication_timer.stop() ## Send all material profiles to the printer. def _sendMaterialProfiles(self): Logger.log("i", "Sending material profiles to printer") # TODO: Might want to move this to a job... for container in ContainerRegistry.getInstance().findInstanceContainers(type="material"): try: xml_data = container.serialize() if xml_data == "" or xml_data is None: continue names = ContainerManager.getInstance().getLinkedMaterials(container.getId()) if names: # There are other materials that share this GUID. if not container.isReadOnly(): continue # If it's not readonly, it's created by user, so skip it. file_name = "none.xml" self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), onFinished=None) except NotImplementedError: # If the material container is not the most "generic" one it can't be serialized an will raise a # NotImplementedError. We can simply ignore these. pass def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): if not self.activePrinter: # No active printer. Unable to write return if self.activePrinter.state not in ["idle", ""]: # Printer is not able to accept commands. return if self._authentication_state != AuthState.Authenticated: # Not authenticated, so unable to send job. return self.writeStarted.emit(self) gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", []) active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate gcode_list = gcode_dict[active_build_plate_id] if not gcode_list: # Unable to find g-code. Nothing to send return self._gcode = gcode_list errors = self._checkForErrors() if errors: text = i18n_catalog.i18nc("@label", "Unable to start a new print job.") informative_text = i18n_catalog.i18nc("@label", "There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. " "Please resolve this issues before continuing.") detailed_text = "" for error in errors: detailed_text += error + "\n" Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), text, informative_text, detailed_text, buttons=QMessageBox.Ok, icon=QMessageBox.Critical, callback = self._messageBoxCallback ) return # Don't continue; Errors must block sending the job to the printer. # There might be multiple things wrong with the configuration. Check these before starting. warnings = self._checkForWarnings() if warnings: text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?") informative_text = i18n_catalog.i18nc("@label", "There is a mismatch between the configuration or calibration of the printer and Cura. " "For the best result, always slice for the PrintCores and materials that are inserted in your printer.") detailed_text = "" for warning in warnings: detailed_text += warning + "\n" Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), text, informative_text, detailed_text, buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, callback=self._messageBoxCallback ) return # No warnings or errors, so we're good to go. self._startPrint() # Notify the UI that a switch to the print monitor should happen Application.getInstance().getController().setActiveStage("MonitorStage") def _startPrint(self): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: 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._sending_gcode = True self._send_gcode_start = time() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data")) self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered) self._progress_message.show() compressed_gcode = self._compressGCode() if compressed_gcode is None: # Abort was called. return file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode, onFinished=self._onPostPrintJobFinished) return def _progressMessageActionTriggered(self, message_id=None, action_id=None): if action_id == "Abort": Logger.log("d", "User aborted sending print to remote.") self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False Application.getInstance().getController().setActiveStage("PrepareStage") def _onPostPrintJobFinished(self, reply): self._progress_message.hide() self._sending_gcode = False def _onUploadPrintJobProgress(self, bytes_sent, bytes_total): if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() if new_progress > self._progress_message.getProgress(): self._progress_message.show() # Ensure that the message is visible. self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: self._progress_message.setProgress(0) self._progress_message.hide() def _messageBoxCallback(self, button): def delayedCallback(): if button == QMessageBox.Yes: self._startPrint() else: Application.getInstance().getController().setActiveStage("PrepareStage") # For some unknown reason Cura on OSX will hang if we do the call back code # immediately without first returning and leaving QML's event system. QTimer.singleShot(100, delayedCallback) def _checkForErrors(self): errors = [] print_information = Application.getInstance().getPrintInformation() if not print_information.materialLengths: Logger.log("w", "There is no material length information. Unable to check for errors.") return errors for index, extruder in enumerate(self.activePrinter.extruders): # Due to airflow issues, both slots must be loaded, regardless if they are actually used or not. if extruder.hotendID == "": # No Printcore loaded. errors.append(i18n_catalog.i18nc("@info:status", "No Printcore loaded in slot {slot_number}".format(slot_number=index + 1))) if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0: # The extruder is by this print. if extruder.activeMaterial is None: # No active material errors.append(i18n_catalog.i18nc("@info:status", "No material loaded in slot {slot_number}".format(slot_number=index + 1))) return errors def _checkForWarnings(self): warnings = [] print_information = Application.getInstance().getPrintInformation() if not print_information.materialLengths: Logger.log("w", "There is no material length information. Unable to check for warnings.") return warnings extruder_manager = ExtruderManager.getInstance() for index, extruder in enumerate(self.activePrinter.extruders): if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0: # The extruder is by this print. # TODO: material length check # Check if the right Printcore is active. variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"}) if variant: if variant.getName() != extruder.hotendID: warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}".format(cura_printcore_name = variant.getName(), remote_printcore_name = extruder.hotendID, extruder_id = index + 1))) else: Logger.log("w", "Unable to find variant.") # Check if the right material is loaded. local_material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"}) if local_material: if extruder.activeMaterial.guid != local_material.getMetaDataEntry("GUID"): Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID")) warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(local_material.getName(), extruder.activeMaterial.name, index + 1)) else: Logger.log("w", "Unable to find material.") return warnings def _update(self): if not super()._update(): return if self._authentication_state == AuthState.NotAuthenticated: if self._authentication_id is None and self._authentication_key is None: # This machine doesn't have any authentication, so request it. self._requestAuthentication() elif self._authentication_id is not None and self._authentication_key is not None: # We have authentication info, but we haven't checked it out yet. Do so now. self._verifyAuthentication() elif self._authentication_state == AuthState.AuthenticationReceived: # We have an authentication, but it's not confirmed yet. self._checkAuthentication() # We don't need authentication for requesting info, so we can go right ahead with requesting this. self.get("printer", onFinished=self._onGetPrinterDataFinished) self.get("print_job", onFinished=self._onGetPrintJobFinished) def _resetAuthenticationRequestedMessage(self): if self._authentication_requested_message: self._authentication_requested_message.hide() self._authentication_timer.stop() self._authentication_counter = 0 def _onAuthenticationTimer(self): self._authentication_counter += 1 self._authentication_requested_message.setProgress( self._authentication_counter / self._max_authentication_counter * 100) if self._authentication_counter > self._max_authentication_counter: self._authentication_timer.stop() Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id) self.setAuthenticationState(AuthState.AuthenticationDenied) self._resetAuthenticationRequestedMessage() self._authentication_failed_message.show() def _verifyAuthentication(self): Logger.log("d", "Attempting to verify authentication") # This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator. self.get("auth/verify", onFinished=self._onVerifyAuthenticationCompleted) def _onVerifyAuthenticationCompleted(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 401: # Something went wrong; We somehow tried to verify authentication without having one. Logger.log("d", "Attempted to verify auth without having one.") self._authentication_id = None self._authentication_key = None self.setAuthenticationState(AuthState.NotAuthenticated) elif status_code == 403 and self._authentication_state != AuthState.Authenticated: # If we were already authenticated, we probably got an older message back all of the sudden. Drop that. Logger.log("d", "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ", self._authentication_state) self.setAuthenticationState(AuthState.AuthenticationDenied) self._authentication_failed_message.show() elif status_code == 200: self.setAuthenticationState(AuthState.Authenticated) def _checkAuthentication(self): Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) self.get("auth/check/" + str(self._authentication_id), onFinished=self._onCheckAuthenticationFinished) def _onCheckAuthenticationFinished(self, reply): if str(self._authentication_id) not in reply.url().toString(): Logger.log("w", "Got an old id response.") # Got response for old authentication ID. return try: data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.") return if data.get("message", "") == "authorized": Logger.log("i", "Authentication was approved") self.setAuthenticationState(AuthState.Authenticated) self._saveAuthentication() # Double check that everything went well. self._verifyAuthentication() # Notify the user. self._resetAuthenticationRequestedMessage() self._authentication_succeeded_message.show() elif data.get("message", "") == "unauthorized": Logger.log("i", "Authentication was denied.") self.setAuthenticationState(AuthState.AuthenticationDenied) self._authentication_failed_message.show() def _saveAuthentication(self): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack: if "network_authentication_key" in global_container_stack.getMetaData(): global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key) else: global_container_stack.addMetaDataEntry("network_authentication_key", self._authentication_key) if "network_authentication_id" in global_container_stack.getMetaData(): global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id) else: global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id) # Force save so we are sure the data is not lost. Application.getInstance().saveStack(global_container_stack) Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) else: Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) def _onRequestAuthenticationFinished(self, reply): try: data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.") self.setAuthenticationState(AuthState.NotAuthenticated) return self.setAuthenticationState(AuthState.AuthenticationReceived) self._authentication_id = data["id"] self._authentication_key = data["key"] Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.", self._authentication_id, self._getSafeAuthKey()) def _requestAuthentication(self): self._authentication_requested_message.show() self._authentication_timer.start() # Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might # give issues. self._authentication_key = None self._authentication_id = None self.post("auth/request", json.dumps({"application": "Cura-" + Application.getInstance().getVersion(), "user": self._getUserName()}).encode(), onFinished=self._onRequestAuthenticationFinished) self.setAuthenticationState(AuthState.AuthenticationRequested) def _onAuthenticationRequired(self, reply, authenticator): if self._authentication_id is not None and self._authentication_key is not None: Logger.log("d", "Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s", self._id, self._authentication_id, self._getSafeAuthKey()) authenticator.setUser(self._authentication_id) authenticator.setPassword(self._authentication_key) else: Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._id) def _onGetPrintJobFinished(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not self._printers: return # Ignore the data for now, we don't have info about a printer yet. printer = self._printers[0] if status_code == 200: try: result = 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 if printer.activePrintJob is None: print_job = PrintJobOutputModel(output_controller=self._output_controller) printer.updateActivePrintJob(print_job) else: print_job = printer.activePrintJob print_job.updateState(result["state"]) print_job.updateTimeElapsed(result["time_elapsed"]) print_job.updateTimeTotal(result["time_total"]) print_job.updateName(result["name"]) elif status_code == 404: # No job found, so delete the active print job (if any!) printer.updateActivePrintJob(None) else: Logger.log("w", "Got status code {status_code} while trying to get printer data".format(status_code=status_code)) def materialHotendChangedMessage(self, callback): Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"), i18n_catalog.i18nc("@label", "Would you like to use your current printer configuration in Cura?"), i18n_catalog.i18nc("@label", "The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."), buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, callback=callback ) def _onGetPrinterDataFinished(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 200: try: result = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log("w", "Received an invalid printer state message: Not valid JSON.") return if not self._printers: # Quickest way to get the firmware version is to grab it from the zeroconf. firmware_version = self._properties.get(b"firmware_version", b"").decode("utf-8") self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version)] self._printers[0].setCamera(NetworkCamera("http://" + self._address + ":8080/?action=stream")) for extruder in self._printers[0].extruders: extruder.activeMaterialChanged.connect(self.materialIdChanged) extruder.hotendIDChanged.connect(self.hotendIdChanged) self.printersChanged.emit() # LegacyUM3 always has a single printer. printer = self._printers[0] printer.updateBedTemperature(result["bed"]["temperature"]["current"]) printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"]) printer.updateState(result["status"]) try: # If we're still handling the request, we should ignore remote for a bit. if not printer.getController().isPreheatRequestInProgress(): printer.updateIsPreheating(result["bed"]["pre_heat"]["active"]) except KeyError: # Older firmwares don't support preheating, so we need to fake it. pass head_position = result["heads"][0]["position"] printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"]) for index in range(0, self._number_of_extruders): temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"] extruder = printer.extruders[index] extruder.updateTargetHotendTemperature(temperatures["target"]) extruder.updateHotendTemperature(temperatures["current"]) material_guid = result["heads"][0]["extruders"][index]["active_material"]["guid"] if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid: # Find matching material (as we need to set brand, type & color) containers = ContainerRegistry.getInstance().findInstanceContainers(type="material", GUID=material_guid) if containers: color = containers[0].getMetaDataEntry("color_code") brand = containers[0].getMetaDataEntry("brand") material_type = containers[0].getMetaDataEntry("material") name = containers[0].getName() else: # Unknown material. color = "#00000000" brand = "Unknown" material_type = "Unknown" name = "Unknown" material = MaterialOutputModel(guid=material_guid, type=material_type, brand=brand, color=color, name = name) extruder.updateActiveMaterial(material) try: hotend_id = result["heads"][0]["extruders"][index]["hotend"]["id"] except KeyError: hotend_id = "" printer.extruders[index].updateHotendID(hotend_id) else: Logger.log("w", "Got status code {status_code} while trying to get printer data".format(status_code = status_code)) ## Convenience function to "blur" out all but the last 5 characters of the auth key. # This can be used to debug print the key, without it compromising the security. def _getSafeAuthKey(self): if self._authentication_key is not None: result = self._authentication_key[-5:] result = "********" + result return result return self._authentication_key
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 ProcessSlicedLayersJob(Job): def __init__(self, layers): super().__init__() self._layers = layers self._scene = Application.getInstance().getController().getScene() self._progress = None self._abort_requested = False ## Aborts the processing of layers. # # This abort is made on a best-effort basis, meaning that the actual # job thread will check once in a while to see whether an abort is # requested and then stop processing by itself. There is no guarantee # that the abort will stop the job any time soon or even at all. def abort(self): self._abort_requested = True def run(self): if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) new_node = SceneNode() ## Remove old layer data (if any) for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData(): if node.callDecoration("getLayerData"): self._scene.getRoot().removeChild(node) Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return settings = Application.getInstance().getMachineManager().getWorkingProfile() mesh = MeshData() layer_data = LayerData.LayerData() layer_count = len(self._layers) # Find the minimum layer number # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # instead simply offset all other layers so the lowest layer is always 0. min_layer_number = 0 for layer in self._layers: if(layer.id < min_layer_number): min_layer_number = layer.id current_layer = 0 for layer in self._layers: abs_layer_number = layer.id + abs(min_layer_number) layer_data.addLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) layer_data.setLayerThickness(abs_layer_number, layer.thickness) for p in range(layer.repeatedMessageCount("polygons")): polygon = layer.getRepeatedMessage("polygons", p) points = numpy.fromstring(polygon.points, dtype="i8") # Convert bytearray to numpy array points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) new_points[:,0] = points[:,0] new_points[:,1] = layer.height new_points[:,2] = -points[:,1] new_points /= 1000 layer_data.addPolygon(abs_layer_number, polygon.type, new_points, polygon.line_width) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 100 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress: self._progress.hide() return if self._progress: self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data layer_data.build() if self._abort_requested: if self._progress: self._progress.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_data) new_node.addDecorator(decorator) new_node.setMeshData(mesh) new_node.setParent(self._scene.getRoot()) # Note: After this we can no longer abort! if not settings.getSettingValue("machine_center_is_zero"): new_node.setPosition(Vector(-settings.getSettingValue("machine_width") / 2, 0.0, settings.getSettingValue("machine_depth") / 2)) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None def _onActiveViewChanged(self): if self.isRunning(): if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": if not self._progress: self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0) if self._progress.getProgress() != 100: self._progress.show() else: if self._progress: self._progress.hide()
class ProcessSlicedLayersJob(Job): def __init__(self, layers): super().__init__() self._layers = layers self._scene = Application.getInstance().getController().getScene() self._progress = None self._abort_requested = False ## Aborts the processing of layers. # # This abort is made on a best-effort basis, meaning that the actual # job thread will check once in a while to see whether an abort is # requested and then stop processing by itself. There is no guarantee # that the abort will stop the job any time soon or even at all. def abort(self): self._abort_requested = True def run(self): start_time = time() if Application.getInstance().getController().getActiveView( ).getPluginId() == "LayerView": self._progress = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) new_node = SceneNode() ## Remove old layer data (if any) for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData(): if node.callDecoration("getLayerData"): self._scene.getRoot().removeChild(node) Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return mesh = MeshData() layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # instead simply offset all other layers so the lowest layer is always 0. min_layer_number = 0 for layer in self._layers: if (layer.id < min_layer_number): min_layer_number = layer.id current_layer = 0 for layer in self._layers: abs_layer_number = layer.id + abs(min_layer_number) layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) layer_data.setLayerThickness(abs_layer_number, layer.thickness) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) extruder = polygon.extruder line_types = numpy.fromstring( polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = line_types.reshape((-1, 1)) points = numpy.fromstring( polygon.points, dtype="f4") # Convert bytearray to numpy array if polygon.point_type == 0: # Point2D points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: # Point3D points = points.reshape((-1, 3)) line_widths = numpy.fromstring( polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) if polygon.point_type == 0: # Point2D new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation new_points[:, 2] = -points[:, 1] else: # Point3D new_points[:, 0] = points[:, 0] new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] this_poly = LayerPolygon.LayerPolygon(layer_data, extruder, line_types, new_points, line_widths) this_poly.buildCache() this_layer.polygons.append(this_poly) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 99 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress: self._progress.hide() return if self._progress: self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data layer_mesh = layer_data.build() if self._abort_requested: if self._progress: self._progress.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) # Set build volume as parent, the build volume can move as a result of raft settings. # It makes sense to set the build volume as parent: the print is actually printed on it. new_node_parent = Application.getInstance().getBuildVolume() new_node.setParent( new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition( Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None Logger.log("d", "Processing layers took %s seconds", time() - start_time) def _onActiveViewChanged(self): if self.isRunning(): if Application.getInstance().getController().getActiveView( ).getPluginId() == "LayerView": if not self._progress: self._progress = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0) if self._progress.getProgress() != 100: self._progress.show() else: if self._progress: self._progress.hide()
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_stream = StringIO() 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" % (CuraApplication.getInstance().getApplicationName(), CuraApplication.getInstance().getVersion(), "OctoPrintPlugin", plugin_version) ) # NetworkedPrinterOutputDevice defines this as string, so we encode this later self._api_prefix = "api/" self._api_header = "X-Api-Key".encode() self._api_key = b"" 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)) self._post_reply = None self._progress_message = None # type: Union[None, Message] self._error_message = None # type: Union[None, Message] self._connection_message = None # type: Union[None, Message] 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) # TODO; Add preference for update intervals self._update_fast_interval = 2000 self._update_slow_interval = 10000 self._update_timer = QTimer() self._update_timer.setInterval(self._update_fast_interval) self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) self._show_camera = False self._camera_mirror = False self._camera_rotation = 0 self._camera_url = "" self._camera_shares_proxy = False self._sd_supported = False self._plugin_data = {} #type: Dict[str, Any] self._output_controller = GenericOutputController(self) def getProperties(self) -> Dict[bytes, bytes]: return self._properties @pyqtSlot(str, result=str) def getProperty(self, key: str) -> str: key_b = key.encode("utf-8") if key_b in self._properties: return self._properties.get(key_b, 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) -> str: return self._id ## Set the API key of this OctoPrint instance def setApiKey(self, api_key: str) -> None: self._api_key = api_key.encode() ## Name of the instance (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def name(self) -> str: return self._name ## Version (as returned from the zeroConf properties) @pyqtProperty(str, constant=True) def octoprintVersion(self) -> str: return self._properties.get(b"version", b"").decode("utf-8") ## IPadress of this instance @pyqtProperty(str, constant=True) def ipAddress(self) -> str: 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) -> str: return self._address ## port of this instance @pyqtProperty(int, constant=True) def port(self) -> int: return self._port ## path of this instance @pyqtProperty(str, constant=True) def path(self) -> str: return self._path ## absolute url of this instance @pyqtProperty(str, constant=True) def baseURL(self) -> str: return self._base_url cameraOrientationChanged = pyqtSignal() @pyqtProperty("QVariantMap", notify=cameraOrientationChanged) def cameraOrientation(self) -> Dict[str, Any]: return { "mirror": self._camera_mirror, "rotation": self._camera_rotation, } cameraUrlChanged = pyqtSignal() @pyqtProperty("QUrl", notify=cameraUrlChanged) def cameraUrl(self) -> QUrl: return QUrl(self._camera_url) def setShowCamera(self, show_camera: bool) -> None: if show_camera != self._show_camera: self._show_camera = show_camera self.showCameraChanged.emit() showCameraChanged = pyqtSignal() @pyqtProperty(bool, notify=showCameraChanged) def showCamera(self) -> bool: return self._show_camera def _update(self) -> None: ## Request 'general' printer data self.get("printer", self._onRequestFinished) ## Request print_job data self.get("job", self._onRequestFinished) def _createEmptyRequest(self, target: str, content_type: Optional[str] = "application/json" ) -> QNetworkRequest: request = QNetworkRequest(QUrl(self._api_url + target)) request.setRawHeader(self._user_agent_header, self._user_agent.encode()) request.setRawHeader(self._api_header, self._api_key) if content_type is not None: request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") if self._basic_auth_data: request.setRawHeader(self._basic_auth_header, self._basic_auth_data) return request def close(self) -> None: self.setConnectionState(ConnectionState.closed) if self._progress_message: self._progress_message.hide() if self._error_message: self._error_message.hide() self._update_timer.stop() ## Start requesting data from the instance def connect(self) -> None: 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.get("settings", self._onRequestFinished) ## Stop requesting data from the instance def disconnect(self) -> None: Logger.log("d", "Connection with instance %s with url %s stopped", self._id, self._base_url) self.close() def pausePrint(self) -> None: self._sendJobCommand("pause") def resumePrint(self) -> None: if not self._printers[0].activePrintJob: return if self._printers[0].activePrintJob.state == "paused": self._sendJobCommand("pause") else: self._sendJobCommand("start") def cancelPrint(self) -> None: self._sendJobCommand("cancel") def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None: global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() if not global_container_stack: return # Make sure post-processing plugin are run on the gcode self.writeStarted.emit(self) # Get the g-code through the GCodeWriter plugin # This produces the same output as "Save to File", adding the print settings to the bottom of the file gcode_writer = cast( MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter")) self._gcode_stream = StringIO() if not gcode_writer.write(self._gcode_stream, None): Logger.log("e", "GCodeWrite failed: %s" % gcode_writer.getInformation()) 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"), "", 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: Optional[str] = None, action_id: Optional[str] = None) -> None: if self._error_message: self._error_message.hide() self._forced_queue = True self._startPrint() def _startPrint(self) -> None: global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() if not global_container_stack: return if self._auto_print and not self._forced_queue: CuraApplication.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"), "", "") self._progress_message.actionTriggered.connect(self._cancelSendGcode) self._progress_message.show() job_name = CuraApplication.getInstance().getPrintInformation( ).jobName.strip() if job_name is "": job_name = "untitled_print" file_name = "%s.gcode" % job_name ## Create multi_part request post_parts = [] # type: List[QHttpPart] ## Create parts (to be placed inside multipart) post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"") post_part.setBody(b"true") post_parts.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") post_parts.append(post_part) post_part = QHttpPart() post_part.setHeader( QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name) post_part.setBody(self._gcode_stream.getvalue().encode()) post_parts.append(post_part) destination = "local" if self._sd_supported and parseBool( global_container_stack.getMetaDataEntry( "octoprint_store_sd", False)): destination = "sdcard" try: ## Post request + data post_request = self._createEmptyRequest("files/" + destination) self._post_reply = self.postFormWithParts( "files/" + destination, post_parts, on_finished=self._onRequestFinished, on_progress=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_stream = StringIO() def _cancelSendGcode(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: 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: str) -> None: 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) -> None: 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 = [] # type: List[str] def _sendJobCommand(self, command: str) -> None: self._sendCommandToApi("job", command) Logger.log("d", "Sent job command to OctoPrint instance: %s", command) def _sendCommandToApi(self, end_point: str, commands: Union[str, List[str]]) -> None: if isinstance(commands, list): data = json.dumps({"commands": commands}) else: data = json.dumps({"command": commands}) self.post(end_point, data, self._onRequestFinished) ## Overloaded from NetworkedPrinterOutputDevice.post() to backport https://github.com/Ultimaker/Cura/pull/4678 def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None: self._validateManager() request = self._createEmptyRequest(target) self._last_request_time = time() if self._manager is not None: reply = self._manager.post(request, data.encode()) if on_progress is not None: reply.uploadProgress.connect(on_progress) self._registerOnFinishedCallback(reply, on_finished) else: Logger.log("e", "Could not find manager.") ## Handler for all requests that have finished. def _onRequestFinished(self, reply: QNetworkReply) -> None: 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] update_pace = self._update_slow_interval if http_status_code == 200: update_pace = self._update_fast_interval 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 elif http_status_code == 502 or http_status_code == 503: printer.updateState("offline") if printer.activePrintJob: printer.activePrintJob.updateState("offline") self.setConnectionText( i18n_catalog.i18nc( "@info:status", "OctoPrint on {0} is not running").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) if update_pace != self._update_timer.interval(): self._update_timer.setInterval(update_pace) 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) self.cameraUrlChanged.emit() 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 self._camera_rotation += 180 elif json_data["webcam"]["flipV"]: self._camera_mirror = True else: self._camera_mirror = False self.cameraOrientationChanged.emit() if "plugins" in json_data: self._plugin_data = json_data["plugins"] 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) if self._progress_message: 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._openOctoPrint) 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) 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: int, bytes_total: int) -> None: if not self._progress_message: return 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 previous_progress = self._progress_message.getProgress() if progress < 100: if previous_progress is not None and progress > previous_progress: 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) -> None: 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 _openOctoPrint(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: QDesktopServices.openUrl(QUrl(self._base_url))
class D3DCloudPrintOutputDevice(OutputDevice): def __init__(self): super().__init__("d3dcloudprint") self.setPriority(1) self.setName("Doodle3D WiFi-Box") self.setShortDescription( i18n_catalog.i18nc("@action:button", "Print with Doodle3D WiFi-Box")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print with Doodle3D WiFi-Box")) self.setIconName("print") self._progress_message = None self.base_url = "http://connect.doodle3d.com" self._manager = QNetworkAccessManager() self._manager.finished.connect(self._onFinished) self.uploading = False def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): if not self.uploading: self.startUpload() def startUpload(self): Logger.log("d", "Upload to Doodle3D connect started") self.uploading = True self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Connecting 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() # Request upload credentials url = QUrl("https://gcodeserver.doodle3d.com/upload") self._post_reply = self._manager.post(QNetworkRequest(url), QByteArray()) self._post_reply.error.connect(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)) def _onProgress(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. 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 Doodle3D Connect"), 0, False, -1) self._progress_message.show() else: self._progress_message.setProgress(0) def _onFinished(self, reply): if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Received a timeout on a request to the G-code server") if self._post_reply: self._post_reply.abort() self._post_reply.uploadProgress.disconnect(self._onProgress) self._post_reply = None self._progress_message.hide() return status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not status_code: Logger.log("d", "A reply from %s did not have status code.", reply.url().toString()) self.uploading = False self._progress_message.hide() return reply_url = reply.url().toString() if "upload" in reply_url: try: json_data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log( "w", "Received invalid upload credentials request reply from Connect: Not valid JSON." ) return self.gcodeId = json_data["data"]["id"] self._progress_message.hide() self.uploadGCode(json_data) elif "amazonaws" in reply_url: if status_code in [200, 201, 202, 204]: self._progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc("@info:status", "File sent to Doodle3D Connect"), 0) self._progress_message.addAction( "open_browser", i18n_catalog.i18nc("@action:button", "Open Connect.."), "globe", i18n_catalog.i18nc( "@info:tooltip", "Open the Doodle3D Connect web interface")) self._progress_message.actionTriggered.connect( self._onMessageActionTriggered) self._progress_message.show() self.uploading = False Logger.log("d", "uploaded to amazon") else: self._progress_message.hide() Logger.log("w", "Unexpected status code in reply from AWS S3") if reply == self._post_reply: self._post_reply = None def _onMessageActionTriggered(self, message, action): if action == "open_browser": QDesktopServices.openUrl( QUrl("%s?uuid=%s" % (self.base_url, self.gcodeId))) elif action == "Cancel": Logger.log("d", "canceled upload to Doodle3D Connect") self._progress_message.hide() if self._post_reply: self._post_reply.abort() self._post_reply = None else: Logger.log("d", "unknown action: %s" % action) def _onNetworkError(self, error): Logger.log( "w", "Network error: %s, %s" % (error, self._post_reply.errorString())) def _onSslError(reply, errors): for error in errors: Logger.log("w", "%s" % error)
class ProcessSlicedLayersJob(Job): def __init__(self, layers): super().__init__() self._layers = layers self._scene = Application.getInstance().getController().getScene() self._progress_message = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._abort_requested = False self._build_plate_number = None ## Aborts the processing of layers. # # This abort is made on a best-effort basis, meaning that the actual # job thread will check once in a while to see whether an abort is # requested and then stop processing by itself. There is no guarantee # that the abort will stop the job any time soon or even at all. def abort(self): self._abort_requested = True def setBuildPlate(self, new_value): self._build_plate_number = new_value def getBuildPlate(self): return self._build_plate_number def run(self): Logger.log( "d", "Processing new layer for build plate %s..." % self._build_plate_number) start_time = time() view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "SimulationView": view.resetLayerData() self._progress_message.show() Job.yieldThread() if self._abort_requested: if self._progress_message: self._progress_message.hide() return Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) # The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice new_node = CuraSceneNode(no_setting_override=True) new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) # Force garbage collection. # For some reason, Python has a tendency to keep the layer data # in memory longer than needed. Forcing the GC to run here makes # sure any old layer data is really cleaned up before adding new. gc.collect() mesh = MeshData() layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # instead simply offset all other layers so the lowest layer is always 0. It could happens that # the first raft layer has value -8 but there are just 4 raft (negative) layers. min_layer_number = 0 negative_layers = 0 for layer in self._layers: if layer.id < min_layer_number: min_layer_number = layer.id if layer.id < 0: negative_layers += 1 current_layer = 0 for layer in self._layers: # Negative layers are offset by the minimum layer number, but the positive layers are just # offset by the number of negative layers so there is no layer gap between raft and model abs_layer_number = layer.id + abs( min_layer_number ) if layer.id < 0 else layer.id + negative_layers layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) layer_data.setLayerThickness(abs_layer_number, layer.thickness) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) extruder = polygon.extruder line_types = numpy.fromstring( polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = line_types.reshape((-1, 1)) points = numpy.fromstring( polygon.points, dtype="f4") # Convert bytearray to numpy array if polygon.point_type == 0: # Point2D points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: # Point3D points = points.reshape((-1, 3)) line_widths = numpy.fromstring( polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_thicknesses = numpy.fromstring( polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array line_thicknesses = line_thicknesses.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_feedrates = numpy.fromstring( polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array line_feedrates = line_feedrates.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. global_container_stack = Application.getInstance( ).getGlobalContainerStack() half_outer_wall_thickness = global_container_stack.getProperty( "wall_line_width_0", "value") / 2 # Adjust layer data to show Raft line type, if it is enabled if global_container_stack.getProperty("blackbelt_raft", "value"): raft_thickness = global_container_stack.getProperty( "blackbelt_raft_thickness", "value") extrusion_started = False for index, segment_type in enumerate(line_types): if points[index + 1][ 1] <= half_outer_wall_thickness + raft_thickness: if segment_type in [ LayerPolygon.LayerPolygon.Inset0Type, LayerPolygon.LayerPolygon.InsetXType ]: line_types[ index] = LayerPolygon.LayerPolygon.SkirtType extrusion_started = True elif extrusion_started: break # Adjust layer data to show Belt Wall feed rate, if it is enabled if global_container_stack.getProperty( "blackbelt_belt_wall_enabled", "value"): belt_wall_feedrate = global_container_stack.getProperty( "blackbelt_belt_wall_speed", "value") belt_wall_indices = [] for index, point in enumerate(points): if point[1] <= half_outer_wall_thickness: if last_point_hit_wall and line_feedrates[ index - 1] > belt_wall_feedrate: belt_wall_indices.append(index) last_point_hit_wall = True else: last_point_hit_wall = False dimensionality = points.shape[1] edited_points = points.flatten() line_types = line_types.flatten() line_widths = line_widths.flatten() line_thicknesses = line_thicknesses.flatten() line_feedrates = line_feedrates.flatten() for index in reversed(belt_wall_indices): edited_points = numpy.insert( edited_points, dimensionality * (index), numpy.append(points[index - 1], points[index])) line_types = numpy.insert(line_types, index, [line_types[index - 1]] * 2) line_widths = numpy.insert( line_widths, index, [line_widths[index - 1]] * 2) line_thicknesses = numpy.insert( line_thicknesses, index, [line_thicknesses[index - 1]] * 2) line_feedrates = numpy.insert(line_feedrates, index - 1, [belt_wall_feedrate] * 2) # Fix shape of adjusted data if polygon.point_type == 0: points = edited_points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: points = edited_points.reshape((-1, 3)) line_types = line_types.reshape((-1, 1)) line_widths = line_widths.reshape((-1, 1)) line_thicknesses = line_thicknesses.reshape((-1, 1)) line_feedrates = line_feedrates.reshape((-1, 1)) # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) if polygon.point_type == 0: # Point2D new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation new_points[:, 2] = -points[:, 1] else: # Point3D new_points[:, 0] = points[:, 0] new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates) this_poly.buildCache() this_layer.polygons.append(this_poly) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 99 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress_message: self._progress_message.hide() return if self._progress_message: self._progress_message.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data # Find out colors per extruder global_container_stack = Application.getInstance( ).getGlobalContainerStack() manager = ExtruderManager.getInstance() extruders = list( manager.getMachineExtruders(global_container_stack.getId())) if extruders: material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) for extruder in extruders: position = int( extruder.getMetaDataEntry("position", default="0")) # Get the position try: default_color = ExtrudersModel.defaultColors[position] except IndexError: default_color = "#e0e000" color_code = extruder.material.getMetaDataEntry( "color_code", default=default_color) color = colorCodeToRGBA(color_code) material_color_map[position, :] = color else: # Single extruder via global stack. material_color_map = numpy.zeros((1, 4), dtype=numpy.float32) color_code = global_container_stack.material.getMetaDataEntry( "color_code", default="#e0e000") color = colorCodeToRGBA(color_code) material_color_map[0, :] = color # We have to scale the colors for compatibility mode if OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance( ).getValue("view/force_layer_view_compatibility_mode")): line_type_brightness = 0.5 # for compatibility mode else: line_type_brightness = 1.0 layer_mesh = layer_data.build(material_color_map, line_type_brightness) if self._abort_requested: if self._progress_message: self._progress_message.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) # Set build volume as parent, the build volume can move as a result of raft settings. # It makes sense to set the build volume as parent: the print is actually printed on it. new_node_parent = Application.getInstance().getBuildVolume() new_node.setParent( new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition( Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) transform = self._scene.getRoot().callDecoration("getTransformMatrix") if transform and transform != Matrix(): transform_matrix = new_node.getLocalTransformation().preMultiply( transform.getInverse()) new_node.setTransformation(transform_matrix) front_offset = self._scene.getRoot().callDecoration( "getSceneFrontOffset") if global_container_stack.getProperty("blackbelt_raft", "value"): front_offset = front_offset - global_container_stack.getProperty("blackbelt_raft_margin", "value") \ - global_container_stack.getProperty("blackbelt_raft_thickness", "value") new_node.translate(Vector(0, 0, front_offset), SceneNode.TransformSpace.World) if self._progress_message: self._progress_message.setProgress(100) if self._progress_message: self._progress_message.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None Logger.log("d", "Processing layers took %s seconds", time() - start_time) def _onActiveViewChanged(self): if self.isRunning(): if Application.getInstance().getController().getActiveView( ).getPluginId() == "SimulationView": if not self._progress_message: self._progress_message = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0, catalog.i18nc("@info:title", "Information")) if self._progress_message.getProgress() != 100: self._progress_message.show() else: if self._progress_message: self._progress_message.hide()
class ProcessSlicedLayersJob(Job): def __init__(self, layers): super().__init__() self._layers = layers self._scene = Application.getInstance().getController().getScene() self._progress = None self._abort_requested = False ## Aborts the processing of layers. # # This abort is made on a best-effort basis, meaning that the actual # job thread will check once in a while to see whether an abort is # requested and then stop processing by itself. There is no guarantee # that the abort will stop the job any time soon or even at all. def abort(self): self._abort_requested = True def run(self): start_time = time() if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) new_node = SceneNode() ## Remove old layer data (if any) for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): node.getParent().removeChild(node) break if self._abort_requested: if self._progress: self._progress.hide() return # Force garbage collection. # For some reason, Python has a tendency to keep the layer data # in memory longer than needed. Forcing the GC to run here makes # sure any old layer data is really cleaned up before adding new. gc.collect() mesh = MeshData() layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # instead simply offset all other layers so the lowest layer is always 0. min_layer_number = 0 for layer in self._layers: if layer.id < min_layer_number: min_layer_number = layer.id current_layer = 0 for layer in self._layers: abs_layer_number = layer.id + abs(min_layer_number) layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) extruder = polygon.extruder line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = line_types.reshape((-1,1)) points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array if polygon.point_type == 0: # Point2D points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: # Point3D points = points.reshape((-1,3)) line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # In the future, line_thicknesses should be given by CuraEngine as well. # Currently the infill layer thickness also translates to line width line_thicknesses = numpy.zeros(line_widths.shape, dtype="f4") line_thicknesses[:] = layer.thickness / 1000 # from micrometer to millimeter # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) if polygon.point_type == 0: # Point2D new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation new_points[:, 2] = -points[:, 1] else: # Point3D new_points[:, 0] = points[:, 0] new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses) this_poly.buildCache() this_layer.polygons.append(this_poly) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 99 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress: self._progress.hide() return if self._progress: self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data # Find out colors per extruder global_container_stack = Application.getInstance().getGlobalContainerStack() manager = ExtruderManager.getInstance() extruders = list(manager.getMachineExtruders(global_container_stack.getId())) if extruders: material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) for extruder in extruders: material = extruder.findContainer({"type": "material"}) position = int(extruder.getMetaDataEntry("position", default="0")) # Get the position color_code = material.getMetaDataEntry("color_code", default="#e0e000") color = colorCodeToRGBA(color_code) material_color_map[position, :] = color else: # Single extruder via global stack. material_color_map = numpy.zeros((1, 4), dtype=numpy.float32) material = global_container_stack.findContainer({"type": "material"}) color_code = "#e0e000" if material: if material.getMetaDataEntry("color_code") is not None: color_code = material.getMetaDataEntry("color_code") color = colorCodeToRGBA(color_code) material_color_map[0, :] = color # We have to scale the colors for compatibility mode if OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")): line_type_brightness = 0.5 # for compatibility mode else: line_type_brightness = 1.0 layer_mesh = layer_data.build(material_color_map, line_type_brightness) if self._abort_requested: if self._progress: self._progress.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) # Set build volume as parent, the build volume can move as a result of raft settings. # It makes sense to set the build volume as parent: the print is actually printed on it. new_node_parent = Application.getInstance().getBuildVolume() new_node.setParent(new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition(Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None Logger.log("d", "Processing layers took %s seconds", time() - start_time) def _onActiveViewChanged(self): if self.isRunning(): if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": if not self._progress: self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0) if self._progress.getProgress() != 100: self._progress.show() else: if self._progress: self._progress.hide()
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): printJobsChanged = pyqtSignal() activePrinterChanged = pyqtSignal() activeCameraUrlChanged = pyqtSignal() receivedPrintJobsChanged = pyqtSignal() # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in. # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions. clusterPrintersChanged = pyqtSignal() def __init__(self, device_id, address, properties, parent = None) -> None: super().__init__(device_id = device_id, address = address, properties=properties, connection_type = ConnectionType.NetworkConnection, parent = parent) self._api_prefix = "/cluster-api/v1/" self._number_of_extruders = 2 self._dummy_lambdas = ("", {}, io.BytesIO()) #type: Tuple[str, Dict, Union[io.StringIO, io.BytesIO]] self._print_jobs = [] # type: List[UM3PrintJobOutputModel] self._received_print_jobs = False # type: bool self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/MonitorStage.qml") # See comments about this hack with the clusterPrintersChanged signal self.printersChanged.connect(self.clusterPrintersChanged) self._accepts_commands = True # type: bool # Cluster does not have authentication, so default to authenticated self._authentication_state = AuthState.Authenticated self._error_message = None # type: Optional[Message] self._write_job_progress_message = None # type: Optional[Message] self._progress_message = None # type: Optional[Message] self._active_printer = None # type: Optional[PrinterOutputModel] self._printer_selection_dialog = None # type: QObject self.setPriority(3) # Make sure the output device gets selected above local file output self.setName(self._id) self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network")) self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network")) self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str] self._finished_jobs = [] # type: List[UM3PrintJobOutputModel] self._cluster_size = int(properties.get(b"cluster_size", 0)) # type: int self._latest_reply_handler = None # type: Optional[QNetworkReply] self._sending_job = None self._active_camera_url = QUrl() # type: QUrl def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: self.writeStarted.emit(self) self.sendMaterialProfiles() # Formats supported by this application (file types that we can actually write). if file_handler: file_formats = file_handler.getSupportedFileTypesWrite() else: file_formats = CuraApplication.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() global_stack = CuraApplication.getInstance().getGlobalContainerStack() # Create a list from the supported file formats string. if not global_stack: Logger.log("e", "Missing global stack!") return machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";") machine_file_formats = [file_type.strip() for file_type in machine_file_formats] # Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format. if "application/x-ufp" not in machine_file_formats and Version(self.firmwareVersion) >= Version("4.4"): machine_file_formats = ["application/x-ufp"] + machine_file_formats # Take the intersection between file_formats and machine_file_formats. format_by_mimetype = {format["mime_type"]: format for format in file_formats} file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats. if len(file_formats) == 0: Logger.log("e", "There are no file formats available to write with!") raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!")) preferred_format = file_formats[0] # Just take the first file format available. if file_handler is not None: writer = file_handler.getWriterByMimeType(cast(str, preferred_format["mime_type"])) else: writer = CuraApplication.getInstance().getMeshFileHandler().getWriterByMimeType(cast(str, preferred_format["mime_type"])) if not writer: Logger.log("e", "Unexpected error when trying to get the FileWriter") return # This function pauses with the yield, waiting on instructions on which printer it needs to print with. if not writer: Logger.log("e", "Missing file or mesh writer!") return self._sending_job = self._sendPrintJob(writer, preferred_format, nodes) if self._sending_job is not None: self._sending_job.send(None) # Start the generator. if len(self._printers) > 1: # We need to ask the user. self._spawnPrinterSelectionDialog() is_job_sent = True else: # Just immediately continue. self._sending_job.send("") # No specifically selected printer. is_job_sent = self._sending_job.send(None) def _spawnPrinterSelectionDialog(self): if self._printer_selection_dialog is None: path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/PrintWindow.qml") self._printer_selection_dialog = CuraApplication.getInstance().createQmlComponent(path, {"OutputDevice": self}) if self._printer_selection_dialog is not None: self._printer_selection_dialog.show() @pyqtProperty(int, constant=True) def clusterSize(self) -> int: return self._cluster_size ## Allows the user to choose a printer to print with from the printer # selection dialogue. # \param target_printer The name of the printer to target. @pyqtSlot(str) def selectPrinter(self, target_printer: str = "") -> None: if self._sending_job is not None: self._sending_job.send(target_printer) @pyqtSlot() def cancelPrintSelection(self) -> None: self._sending_gcode = False ## Greenlet to send a job to the printer over the network. # # This greenlet gets called asynchronously in requestWrite. It is a # greenlet in order to optionally wait for selectPrinter() to select a # printer. # The greenlet yields exactly three times: First time None, # \param writer The file writer to use to create the data. # \param preferred_format A dictionary containing some information about # what format to write to. This is necessary to create the correct buffer # types and file extension and such. def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict, nodes: List[SceneNode]): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: self._error_message = Message( i18n_catalog.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job.")) self._error_message.show() yield #Wait on the user to select a target printer. yield #Wait for the write job to be finished. yield False #Return whether this was a success or not. yield #Prevent StopIteration. self._sending_gcode = True target_printer = yield #Potentially wait on the user to select a target printer. # Using buffering greatly reduces the write time for many lines of gcode stream = io.BytesIO() # type: Union[io.BytesIO, io.StringIO]# Binary mode. if preferred_format["mode"] == FileWriter.OutputMode.TextMode: stream = io.StringIO() job = WriteFileJob(writer, stream, nodes, preferred_format["mode"]) self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1, title = i18n_catalog.i18nc("@info:title", "Sending Data"), use_inactivity_timer = False) self._write_job_progress_message.show() self._dummy_lambdas = (target_printer, preferred_format, stream) job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished) job.start() yield True # Return that we had success! yield # To prevent having to catch the StopIteration exception. def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None: if self._write_job_progress_message: self._write_job_progress_message.hide() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1, title = i18n_catalog.i18nc("@info:title", "Sending Data")) self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = None, description = "") self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered) self._progress_message.show() parts = [] target_printer, preferred_format, stream = self._dummy_lambdas # If a specific printer was selected, it should be printed with that machine. if target_printer: target_printer = self._printer_uuid_to_unique_name_mapping[target_printer] parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain")) # Add user name to the print_job parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) file_name = CuraApplication.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"] output = stream.getvalue() # Either str or bytes depending on the output mode. if isinstance(stream, io.StringIO): output = cast(str, output).encode("utf-8") output = cast(bytes, output) parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output)) self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, on_finished = self._onPostPrintJobFinished, on_progress = self._onUploadPrintJobProgress) @pyqtProperty(QObject, notify = activePrinterChanged) def activePrinter(self) -> Optional[PrinterOutputModel]: return self._active_printer @pyqtSlot(QObject) def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None: if self._active_printer != printer: self._active_printer = printer self.activePrinterChanged.emit() @pyqtProperty(QUrl, notify = activeCameraUrlChanged) def activeCameraUrl(self) -> "QUrl": return self._active_camera_url @pyqtSlot(QUrl) def setActiveCameraUrl(self, camera_url: "QUrl") -> None: if self._active_camera_url != camera_url: self._active_camera_url = camera_url self.activeCameraUrlChanged.emit() def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None: if self._progress_message: self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None: if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() if self._progress_message and 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) # If successfully sent: if bytes_sent == bytes_total: # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to # the monitor tab. self._success_message = Message( i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."), lifetime=5, dismissable=True, title=i18n_catalog.i18nc("@info:title", "Data Sent")) self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Monitor"), icon=None, description="") self._success_message.actionTriggered.connect(self._successMessageActionTriggered) self._success_message.show() else: if self._progress_message is not None: self._progress_message.setProgress(0) self._progress_message.hide() def _progressMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: if action_id == "Abort": Logger.log("d", "User aborted sending print to remote.") if self._progress_message is not None: self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False CuraApplication.getInstance().getController().setActiveStage("PrepareStage") # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request # the "reply" should be disconnected if self._latest_reply_handler: self._latest_reply_handler.disconnect() self._latest_reply_handler = None def _successMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: if action_id == "View": CuraApplication.getInstance().getController().setActiveStage("MonitorStage") @pyqtSlot() def openPrintJobControlPanel(self) -> None: Logger.log("d", "Opening print job control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs")) @pyqtSlot() def openPrinterControlPanel(self) -> None: Logger.log("d", "Opening printer control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers")) @pyqtProperty("QVariantList", notify = printJobsChanged) def printJobs(self)-> List[UM3PrintJobOutputModel]: return self._print_jobs @pyqtProperty(bool, notify = receivedPrintJobsChanged) def receivedPrintJobs(self) -> bool: return self._received_print_jobs @pyqtProperty("QVariantList", notify = printJobsChanged) def queuedPrintJobs(self) -> List[UM3PrintJobOutputModel]: return [print_job for print_job in self._print_jobs if print_job.state == "queued" or print_job.state == "error"] @pyqtProperty("QVariantList", notify = printJobsChanged) def activePrintJobs(self) -> List[UM3PrintJobOutputModel]: return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"] @pyqtProperty("QVariantList", notify = clusterPrintersChanged) def connectedPrintersTypeCount(self) -> List[Dict[str, str]]: printer_count = {} # type: Dict[str, int] for printer in self._printers: if printer.type in printer_count: printer_count[printer.type] += 1 else: printer_count[printer.type] = 1 result = [] for machine_type in printer_count: result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])}) return result @pyqtProperty("QVariantList", notify=clusterPrintersChanged) def printers(self): return self._printers @pyqtSlot(int, result = str) def formatDuration(self, seconds: int) -> str: return Duration(seconds).getDisplayString(DurationFormat.Format.Short) @pyqtSlot(int, result = str) def getTimeCompleted(self, time_remaining: int) -> str: current_time = time() datetime_completed = 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: int) -> str: current_time = time() completed = datetime.fromtimestamp(current_time + time_remaining) today = datetime.fromtimestamp(current_time) # If finishing date is more than 7 days out, using "Mon Dec 3 at HH:MM" format if completed.toordinal() > today.toordinal() + 7: return completed.strftime("%a %b ") + "{day}".format(day=completed.day) # If finishing date is within the next week, use "Monday at HH:MM" format elif completed.toordinal() > today.toordinal() + 1: return completed.strftime("%a") # If finishing tomorrow, use "tomorrow at HH:MM" format elif completed.toordinal() > today.toordinal(): return "tomorrow" # If finishing today, use "today at HH:MM" format else: return "today" @pyqtSlot(str) def sendJobToTop(self, print_job_uuid: str) -> None: # This function is part of the output device (and not of the printjob output model) as this type of operation # is a modification of the cluster queue and not of the actual job. data = "{\"to_position\": 0}" self.put("print_jobs/{uuid}/move_to_position".format(uuid = print_job_uuid), data, on_finished=None) @pyqtSlot(str) def deleteJobFromQueue(self, print_job_uuid: str) -> None: # This function is part of the output device (and not of the printjob output model) as this type of operation # is a modification of the cluster queue and not of the actual job. self.delete("print_jobs/{uuid}".format(uuid = print_job_uuid), on_finished=None) @pyqtSlot(str) def forceSendJob(self, print_job_uuid: str) -> None: data = "{\"force\": true}" self.put("print_jobs/{uuid}".format(uuid=print_job_uuid), data, on_finished=None) def _printJobStateChanged(self) -> None: username = self._getUserName() if username is None: return # We only want to show notifications if username is set. finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"] newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username] for job in newly_finished_jobs: if job.assignedPrinter: job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name)) else: job_completed_text = i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.".format(job_name = job.name)) job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished")) job_completed_message.show() # Ensure UI gets updated self.printJobsChanged.emit() # Keep a list of all completed jobs so we know if something changed next time. self._finished_jobs = finished_jobs ## Called when the connection to the cluster changes. def connect(self) -> None: super().connect() self.sendMaterialProfiles() def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None: reply_url = reply.url().toString() uuid = reply_url[reply_url.find("print_jobs/")+len("print_jobs/"):reply_url.rfind("/preview_image")] print_job = findByKey(self._print_jobs, uuid) if print_job: image = QImage() image.loadFromData(reply.readAll()) print_job.updatePreviewImage(image) def _update(self) -> None: super()._update() self.get("printers/", on_finished = self._onGetPrintersDataFinished) self.get("print_jobs/", on_finished = self._onGetPrintJobsFinished) for print_job in self._print_jobs: if print_job.getPreviewImage() is None: self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished) def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None: self._received_print_jobs = True self.receivedPrintJobsChanged.emit() if not checkValidGetReply(reply): return result = loadJsonFromReply(reply) if result is None: return print_jobs_seen = [] job_list_changed = False for idx, print_job_data in enumerate(result): print_job = findByKey(self._print_jobs, print_job_data["uuid"]) if print_job is None: print_job = self._createPrintJobModel(print_job_data) job_list_changed = True elif not job_list_changed: # Check if the order of the jobs has changed since the last check if self._print_jobs.index(print_job) != idx: job_list_changed = True self._updatePrintJob(print_job, print_job_data) if print_job.state != "queued" and print_job.state != "error": # Print job should be assigned to a printer. if print_job.state in ["failed", "finished", "aborted", "none"]: # Print job was already completed, so don't attach it to a printer. printer = None else: printer = self._getPrinterByKey(print_job_data["printer_uuid"]) else: # The job can "reserve" a printer if some changes are required. printer = self._getPrinterByKey(print_job_data["assigned_to"]) if printer: printer.updateActivePrintJob(print_job) print_jobs_seen.append(print_job) # Check what jobs need to be removed. removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen] for removed_job in removed_jobs: job_list_changed = job_list_changed or self._removeJob(removed_job) if job_list_changed: # Override the old list with the new list (either because jobs were removed / added or order changed) self._print_jobs = print_jobs_seen self.printJobsChanged.emit() # Do a single emit for all print job changes. def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None: if not checkValidGetReply(reply): return result = loadJsonFromReply(reply) if result is None: return printer_list_changed = False printers_seen = [] for printer_data in result: printer = findByKey(self._printers, printer_data["uuid"]) if printer is None: printer = self._createPrinterModel(printer_data) printer_list_changed = True printers_seen.append(printer) self._updatePrinter(printer, printer_data) removed_printers = [printer for printer in self._printers if printer not in printers_seen] for printer in removed_printers: self._removePrinter(printer) if removed_printers or printer_list_changed: self.printersChanged.emit() def _createPrinterModel(self, data: Dict[str, Any]) -> PrinterOutputModel: printer = PrinterOutputModel(output_controller = ClusterUM3PrinterOutputController(self), number_of_extruders = self._number_of_extruders) printer.setCameraUrl(QUrl("http://" + data["ip_address"] + ":8080/?action=stream")) self._printers.append(printer) return printer def _createPrintJobModel(self, data: Dict[str, Any]) -> UM3PrintJobOutputModel: print_job = UM3PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self), key=data["uuid"], name= data["name"]) configuration = ConfigurationModel() extruders = [ExtruderConfigurationModel(position = idx) for idx in range(0, self._number_of_extruders)] for index in range(0, self._number_of_extruders): try: extruder_data = data["configuration"][index] except IndexError: continue extruder = extruders[int(data["configuration"][index]["extruder_index"])] extruder.setHotendID(extruder_data.get("print_core_id", "")) extruder.setMaterial(self._createMaterialOutputModel(extruder_data.get("material", {}))) configuration.setExtruderConfigurations(extruders) print_job.updateConfiguration(configuration) print_job.setCompatibleMachineFamilies(data.get("compatible_machine_families", [])) print_job.stateChanged.connect(self._printJobStateChanged) return print_job def _updatePrintJob(self, print_job: UM3PrintJobOutputModel, data: Dict[str, Any]) -> None: print_job.updateTimeTotal(data["time_total"]) print_job.updateTimeElapsed(data["time_elapsed"]) impediments_to_printing = data.get("impediments_to_printing", []) print_job.updateOwner(data["owner"]) status_set_by_impediment = False for impediment in impediments_to_printing: if impediment["severity"] == "UNFIXABLE": status_set_by_impediment = True print_job.updateState("error") break if not status_set_by_impediment: print_job.updateState(data["status"]) print_job.updateConfigurationChanges(self._createConfigurationChanges(data["configuration_changes_required"])) def _createConfigurationChanges(self, data: List[Dict[str, Any]]) -> List[ConfigurationChangeModel]: result = [] for change in data: result.append(ConfigurationChangeModel(type_of_change=change["type_of_change"], index=change["index"], target_name=change["target_name"], origin_name=change["origin_name"])) return result def _createMaterialOutputModel(self, material_data: Dict[str, Any]) -> "MaterialOutputModel": material_manager = CuraApplication.getInstance().getMaterialManager() material_group_list = None # Avoid crashing if there is no "guid" field in the metadata material_guid = material_data.get("guid") if material_guid: material_group_list = material_manager.getMaterialGroupListByGUID(material_guid) # This can happen if the connected machine has no material in one or more extruders (if GUID is empty), or the # material is unknown to Cura, so we should return an "empty" or "unknown" material model. if material_group_list is None: material_name = i18n_catalog.i18nc("@label:material", "Empty") if len(material_data.get("guid", "")) == 0 \ else i18n_catalog.i18nc("@label:material", "Unknown") return MaterialOutputModel(guid = material_data.get("guid", ""), type = material_data.get("type", ""), color = material_data.get("color", ""), brand = material_data.get("brand", ""), name = material_data.get("name", material_name) ) # Sort the material groups by "is_read_only = True" first, and then the name alphabetically. read_only_material_group_list = list(filter(lambda x: x.is_read_only, material_group_list)) non_read_only_material_group_list = list(filter(lambda x: not x.is_read_only, material_group_list)) material_group = None if read_only_material_group_list: read_only_material_group_list = sorted(read_only_material_group_list, key = lambda x: x.name) material_group = read_only_material_group_list[0] elif non_read_only_material_group_list: non_read_only_material_group_list = sorted(non_read_only_material_group_list, key = lambda x: x.name) material_group = non_read_only_material_group_list[0] if material_group: container = material_group.root_material_node.getContainer() color = container.getMetaDataEntry("color_code") brand = container.getMetaDataEntry("brand") material_type = container.getMetaDataEntry("material") name = container.getName() else: Logger.log("w", "Unable to find material with guid {guid}. Using data as provided by cluster".format( guid=material_data["guid"])) color = material_data["color"] brand = material_data["brand"] material_type = material_data["material"] name = i18n_catalog.i18nc("@label:material", "Empty") if material_data["material"] == "empty" \ else i18n_catalog.i18nc("@label:material", "Unknown") return MaterialOutputModel(guid = material_data["guid"], type = material_type, brand = brand, color = color, name = name) def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None: # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer. # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping. self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"] definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"]) if not definitions: Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"]) return machine_definition = definitions[0] printer.updateName(data["friendly_name"]) printer.updateKey(data["uuid"]) printer.updateType(data["machine_variant"]) # Do not store the build plate information that comes from connect if the current printer has not build plate information if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False): printer.updateBuildplateName(data["build_plate"]["type"]) if not data["enabled"]: printer.updateState("disabled") else: printer.updateState(data["status"]) for index in range(0, self._number_of_extruders): extruder = printer.extruders[index] try: extruder_data = data["configuration"][index] except IndexError: break extruder.updateHotendID(extruder_data.get("print_core_id", "")) material_data = extruder_data["material"] if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]: material = self._createMaterialOutputModel(material_data) extruder.updateActiveMaterial(material) def _removeJob(self, job: UM3PrintJobOutputModel) -> bool: if job not in self._print_jobs: return False if job.assignedPrinter: job.assignedPrinter.updateActivePrintJob(None) job.stateChanged.disconnect(self._printJobStateChanged) self._print_jobs.remove(job) return True def _removePrinter(self, printer: PrinterOutputModel) -> None: self._printers.remove(printer) if self._active_printer == printer: self._active_printer = None self.activePrinterChanged.emit() ## Sync the material profiles in Cura with the printer. # # This gets called when connecting to a printer as well as when sending a # print. def sendMaterialProfiles(self) -> None: job = SendMaterialJob(device = self) job.run()
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 ProcessSlicedLayersJob(Job): def __init__(self, layers): super().__init__() self._layers = layers self._scene = Application.getInstance().getController().getScene() self._progress = None self._abort_requested = False ## Aborts the processing of layers. # # This abort is made on a best-effort basis, meaning that the actual # job thread will check once in a while to see whether an abort is # requested and then stop processing by itself. There is no guarantee # that the abort will stop the job any time soon or even at all. def abort(self): self._abort_requested = True def run(self): start_time = time() if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) new_node = SceneNode() ## Remove old layer data (if any) for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): node.getParent().removeChild(node) break if self._abort_requested: if self._progress: self._progress.hide() return # Force garbage collection. # For some reason, Python has a tendency to keep the layer data # in memory longer than needed. Forcing the GC to run here makes # sure any old layer data is really cleaned up before adding new. gc.collect() mesh = MeshData() layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # instead simply offset all other layers so the lowest layer is always 0. min_layer_number = 0 for layer in self._layers: if layer.id < min_layer_number: min_layer_number = layer.id current_layer = 0 for layer in self._layers: abs_layer_number = layer.id + abs(min_layer_number) layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) extruder = polygon.extruder line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = line_types.reshape((-1,1)) points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array if polygon.point_type == 0: # Point2D points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: # Point3D points = points.reshape((-1,3)) line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # In the future, line_thicknesses should be given by CuraEngine as well. # Currently the infill layer thickness also translates to line width line_thicknesses = numpy.zeros(line_widths.shape, dtype="f4") line_thicknesses[:] = layer.thickness / 1000 # from micrometer to millimeter # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) if polygon.point_type == 0: # Point2D new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation new_points[:, 2] = -points[:, 1] else: # Point3D new_points[:, 0] = points[:, 0] new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses) this_poly.buildCache() this_layer.polygons.append(this_poly) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 99 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress: self._progress.hide() return if self._progress: self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data # Find out colors per extruder global_container_stack = Application.getInstance().getGlobalContainerStack() manager = ExtruderManager.getInstance() extruders = list(manager.getMachineExtruders(global_container_stack.getId())) if extruders: material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) for extruder in extruders: position = int(extruder.getMetaDataEntry("position", default="0")) # Get the position try: default_color = ExtrudersModel.defaultColors[position] except KeyError: default_color = "#e0e000" color_code = extruder.material.getMetaDataEntry("color_code", default=default_color) color = colorCodeToRGBA(color_code) material_color_map[position, :] = color else: # Single extruder via global stack. material_color_map = numpy.zeros((1, 4), dtype=numpy.float32) color_code = global_container_stack.material.getMetaDataEntry("color_code", default="#e0e000") color = colorCodeToRGBA(color_code) material_color_map[0, :] = color # We have to scale the colors for compatibility mode if OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")): line_type_brightness = 0.5 # for compatibility mode else: line_type_brightness = 1.0 layer_mesh = layer_data.build(material_color_map, line_type_brightness) if self._abort_requested: if self._progress: self._progress.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) # Set build volume as parent, the build volume can move as a result of raft settings. # It makes sense to set the build volume as parent: the print is actually printed on it. new_node_parent = Application.getInstance().getBuildVolume() new_node.setParent(new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition(Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None Logger.log("d", "Processing layers took %s seconds", time() - start_time) def _onActiveViewChanged(self): if self.isRunning(): if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": if not self._progress: self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0) if self._progress.getProgress() != 100: self._progress.show() else: if self._progress: self._progress.hide()
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): printJobsChanged = pyqtSignal() activePrinterChanged = pyqtSignal() # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in. # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions. clusterPrintersChanged = pyqtSignal() def __init__(self, device_id, address, properties, parent=None): super().__init__(device_id=device_id, address=address, properties=properties, parent=parent) self._api_prefix = "/cluster-api/v1/" self._number_of_extruders = 2 self._print_jobs = [] 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") # See comments about this hack with the clusterPrintersChanged signal self.printersChanged.connect(self.clusterPrintersChanged) self._accepts_commands = True # Cluster does not have authentication, so default to authenticated self._authentication_state = AuthState.Authenticated self._error_message = None self._progress_message = None self._active_printer = None # type: Optional[PrinterOutputModel] self._printer_selection_dialog = None self.setPriority( 3 ) # Make sure the output device gets selected above local file output self.setName(self._id) self.setShortDescription( i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print over network")) self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected over the network")) self._printer_uuid_to_unique_name_mapping = {} self._finished_jobs = [] self._cluster_size = int(properties.get(b"cluster_size", 0)) def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): self.writeStarted.emit(self) gcode_dict = getattr( Application.getInstance().getController().getScene(), "gcode_dict", []) active_build_plate_id = Application.getInstance( ).getMultiBuildPlateModel().activeBuildPlate gcode_list = gcode_dict[active_build_plate_id] if not gcode_list: # Unable to find g-code. Nothing to send return self._gcode = gcode_list if len(self._printers) > 1: self._spawnPrinterSelectionDialog() else: self.sendPrintJob() # Notify the UI that a switch to the print monitor should happen Application.getInstance().getController().setActiveStage( "MonitorStage") def _spawnPrinterSelectionDialog(self): if self._printer_selection_dialog is None: path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml") self._printer_selection_dialog = Application.getInstance( ).createQmlComponent(path, {"OutputDevice": self}) if self._printer_selection_dialog is not None: self._printer_selection_dialog.show() @pyqtProperty(int, constant=True) def clusterSize(self): return self._cluster_size @pyqtSlot() @pyqtSlot(str) def sendPrintJob(self, target_printer=""): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: 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._sending_gcode = True self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data")) self._progress_message.addAction( "Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect( self._progressMessageActionTriggered) self._progress_message.show() compressed_gcode = self._compressGCode() if compressed_gcode is None: # Abort was called. return parts = [] # If a specific printer was selected, it should be printed with that machine. if target_printer: target_printer = self._printer_uuid_to_unique_name_mapping[ target_printer] parts.append( self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain")) # Add user name to the print_job parts.append( self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) file_name = "%s.gcode.gz" % Application.getInstance( ).getPrintInformation().jobName parts.append( self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, compressed_gcode)) self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress) @pyqtProperty(QObject, notify=activePrinterChanged) def activePrinter(self) -> Optional["PrinterOutputModel"]: return self._active_printer @pyqtSlot(QObject) def setActivePrinter(self, printer): if self._active_printer != printer: if self._active_printer and self._active_printer.camera: self._active_printer.camera.stop() self._active_printer = printer self.activePrinterChanged.emit() def _onPostPrintJobFinished(self, reply): self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False def _onUploadPrintJobProgress(self, bytes_sent, bytes_total): if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() if new_progress > self._progress_message.getProgress(): self._progress_message.show( ) # Ensure that the message is visible. self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: self._progress_message.setProgress(0) self._progress_message.hide() def _progressMessageActionTriggered(self, message_id=None, action_id=None): if action_id == "Abort": Logger.log("d", "User aborted sending print to remote.") self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False Application.getInstance().getController().setActiveStage( "PrepareStage") @pyqtSlot() def openPrintJobControlPanel(self): Logger.log("d", "Opening print job control panel...") QDesktopServices.openUrl( QUrl("http://" + self._address + "/print_jobs")) @pyqtSlot() def openPrinterControlPanel(self): Logger.log("d", "Opening printer control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers")) @pyqtProperty("QVariantList", notify=printJobsChanged) def printJobs(self): return self._print_jobs @pyqtProperty("QVariantList", notify=printJobsChanged) def queuedPrintJobs(self): return [ print_job for print_job in self._print_jobs if print_job.assignedPrinter is None or print_job.state == "queued" ] @pyqtProperty("QVariantList", notify=printJobsChanged) def activePrintJobs(self): return [ print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued" ] @pyqtProperty("QVariantList", notify=clusterPrintersChanged) def connectedPrintersTypeCount(self): printer_count = {} for printer in self._printers: if printer.type in printer_count: printer_count[printer.type] += 1 else: printer_count[printer.type] = 1 result = [] for machine_type in printer_count: result.append({ "machine_type": machine_type, "count": printer_count[machine_type] }) return result @pyqtSlot(int, result=str) def formatDuration(self, seconds): return Duration(seconds).getDisplayString(DurationFormat.Format.Short) @pyqtSlot(int, result=str) def getTimeCompleted(self, time_remaining): current_time = time() datetime_completed = 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() datetime_completed = datetime.fromtimestamp(current_time + time_remaining) return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper() def _printJobStateChanged(self): username = self._getUserName() if username is None: return # We only want to show notifications if username is set. finished_jobs = [ job for job in self._print_jobs if job.state == "wait_cleanup" ] newly_finished_jobs = [ job for job in finished_jobs if job not in self._finished_jobs and job.owner == username ] for job in newly_finished_jobs: if job.assignedPrinter: job_completed_text = i18n_catalog.i18nc( "@info:status", "Printer '{printer_name}' has finished printing '{job_name}'." .format(printer_name=job.assignedPrinter.name, job_name=job.name)) else: job_completed_text = i18n_catalog.i18nc( "@info:status", "The print job '{job_name}' was finished.".format( job_name=job.name)) job_completed_message = Message(text=job_completed_text, title=i18n_catalog.i18nc( "@info:status", "Print finished")) job_completed_message.show() # Ensure UI gets updated self.printJobsChanged.emit() # Keep a list of all completed jobs so we know if something changed next time. self._finished_jobs = finished_jobs def _update(self): if not super()._update(): return self.get("printers/", onFinished=self._onGetPrintersDataFinished) self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished) def _onGetPrintJobsFinished(self, reply: QNetworkReply): if not checkValidGetReply(reply): return result = loadJsonFromReply(reply) if result is None: return print_jobs_seen = [] job_list_changed = False for print_job_data in result: print_job = findByKey(self._print_jobs, print_job_data["uuid"]) if print_job is None: print_job = self._createPrintJobModel(print_job_data) job_list_changed = True self._updatePrintJob(print_job, print_job_data) if print_job.state != "queued": # Print job should be assigned to a printer. if print_job.state == "failed": # Print job was failed, so don't attach it to a printer. printer = None else: printer = self._getPrinterByKey( print_job_data["printer_uuid"]) else: # The job can "reserve" a printer if some changes are required. printer = self._getPrinterByKey(print_job_data["assigned_to"]) if printer: printer.updateActivePrintJob(print_job) print_jobs_seen.append(print_job) # Check what jobs need to be removed. removed_jobs = [ print_job for print_job in self._print_jobs if print_job not in print_jobs_seen ] for removed_job in removed_jobs: job_list_changed |= self._removeJob(removed_job) if job_list_changed: self.printJobsChanged.emit( ) # Do a single emit for all print job changes. def _onGetPrintersDataFinished(self, reply: QNetworkReply): if not checkValidGetReply(reply): return result = loadJsonFromReply(reply) if result is None: return printer_list_changed = False printers_seen = [] for printer_data in result: printer = findByKey(self._printers, printer_data["uuid"]) if printer is None: printer = self._createPrinterModel(printer_data) printer_list_changed = True printers_seen.append(printer) self._updatePrinter(printer, printer_data) removed_printers = [ printer for printer in self._printers if printer not in printers_seen ] for printer in removed_printers: self._removePrinter(printer) if removed_printers or printer_list_changed: self.printersChanged.emit() def _createPrinterModel(self, data): printer = PrinterOutputModel( output_controller=ClusterUM3PrinterOutputController(self), number_of_extruders=self._number_of_extruders) printer.setCamera( NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream")) self._printers.append(printer) return printer def _createPrintJobModel(self, data): print_job = PrintJobOutputModel( output_controller=ClusterUM3PrinterOutputController(self), key=data["uuid"], name=data["name"]) print_job.stateChanged.connect(self._printJobStateChanged) self._print_jobs.append(print_job) return print_job def _updatePrintJob(self, print_job, data): print_job.updateTimeTotal(data["time_total"]) print_job.updateTimeElapsed(data["time_elapsed"]) print_job.updateState(data["status"]) print_job.updateOwner(data["owner"]) def _updatePrinter(self, printer, data): # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer. # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping. self._printer_uuid_to_unique_name_mapping[ data["uuid"]] = data["unique_name"] printer.updateName(data["friendly_name"]) printer.updateKey(data["uuid"]) printer.updateType(data["machine_variant"]) if not data["enabled"]: printer.updateState("disabled") else: printer.updateState(data["status"]) for index in range(0, self._number_of_extruders): extruder = printer.extruders[index] try: extruder_data = data["configuration"][index] except IndexError: break extruder.updateHotendID(extruder_data.get("print_core_id", "")) material_data = extruder_data["material"] if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data[ "guid"]: containers = ContainerRegistry.getInstance( ).findInstanceContainers(type="material", GUID=material_data["guid"]) if containers: color = containers[0].getMetaDataEntry("color_code") brand = containers[0].getMetaDataEntry("brand") material_type = containers[0].getMetaDataEntry("material") name = containers[0].getName() else: Logger.log( "w", "Unable to find material with guid {guid}. Using data as provided by cluster" .format(guid=material_data["guid"])) color = material_data["color"] brand = material_data["brand"] material_type = material_data["material"] name = "Empty" if material_data[ "material"] == "empty" else "Unknown" material = MaterialOutputModel(guid=material_data["guid"], type=material_type, brand=brand, color=color, name=name) extruder.updateActiveMaterial(material) def _removeJob(self, job): if job not in self._print_jobs: return False if job.assignedPrinter: job.assignedPrinter.updateActivePrintJob(None) job.stateChanged.disconnect(self._printJobStateChanged) self._print_jobs.remove(job) return True def _removePrinter(self, printer): self._printers.remove(printer) if self._active_printer == printer: self._active_printer = None self.activePrinterChanged.emit()
class ProcessSlicedLayersJob(Job): def __init__(self, layers): super().__init__() self._layers = layers self._scene = Application.getInstance().getController().getScene() self._progress = None self._abort_requested = False ## Aborts the processing of layers. # # This abort is made on a best-effort basis, meaning that the actual # job thread will check once in a while to see whether an abort is # requested and then stop processing by itself. There is no guarantee # that the abort will stop the job any time soon or even at all. def abort(self): self._abort_requested = True def run(self): if Application.getInstance().getController().getActiveView( ).getPluginId() == "LayerView": self._progress = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._progress.show() Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) object_id_map = {} new_node = SceneNode() ## Remove old layer data (if any) for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData(): if node.callDecoration("getLayerData"): self._scene.getRoot().removeChild(node) Job.yieldThread() if self._abort_requested: if self._progress: self._progress.hide() return settings = Application.getInstance().getMachineManager( ).getWorkingProfile() mesh = MeshData() layer_data = LayerData.LayerData() layer_count = len(self._layers) current_layer = 0 for layer in self._layers: layer_data.addLayer(layer.id) layer_data.setLayerHeight(layer.id, layer.height) layer_data.setLayerThickness(layer.id, layer.thickness) for p in range(layer.repeatedMessageCount("polygons")): polygon = layer.getRepeatedMessage("polygons", p) points = numpy.fromstring( polygon.points, dtype="i8") # Convert bytearray to numpy array points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height new_points[:, 2] = -points[:, 1] new_points /= 1000 layer_data.addPolygon(layer.id, polygon.type, new_points, polygon.line_width) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 100 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress: self._progress.hide() return if self._progress: self._progress.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data layer_data.build() if self._abort_requested: if self._progress: self._progress.hide() return #Add layerdata decorator to scene node to indicate that the node has layerdata decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_data) new_node.addDecorator(decorator) new_node.setMeshData(mesh) new_node.setParent( self._scene.getRoot()) #Note: After this we can no longer abort! if not settings.getSettingValue("machine_center_is_zero"): new_node.setPosition( Vector(-settings.getSettingValue("machine_width") / 2, 0.0, settings.getSettingValue("machine_depth") / 2)) if self._progress: self._progress.setProgress(100) view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "LayerView": view.resetLayerData() if self._progress: self._progress.hide() def _onActiveViewChanged(self): if self.isRunning(): if Application.getInstance().getController().getActiveView( ).getPluginId() == "LayerView": if not self._progress: self._progress = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0) if self._progress.getProgress() != 100: self._progress.show() else: if self._progress: self._progress.hide()
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 ProcessSlicedLayersJob(Job): def __init__(self, layers): super().__init__() self._layers = layers self._scene = Application.getInstance().getController().getScene() self._progress_message = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._abort_requested = False self._build_plate_number = None def abort(self): """Aborts the processing of layers. This abort is made on a best-effort basis, meaning that the actual job thread will check once in a while to see whether an abort is requested and then stop processing by itself. There is no guarantee that the abort will stop the job any time soon or even at all. """ self._abort_requested = True def setBuildPlate(self, new_value): self._build_plate_number = new_value def getBuildPlate(self): return self._build_plate_number def run(self): Logger.log( "d", "Processing new layer for build plate %s..." % self._build_plate_number) start_time = time() view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "SimulationView": view.resetLayerData() self._progress_message.show() Job.yieldThread() if self._abort_requested: if self._progress_message: self._progress_message.hide() return Application.getInstance().getController().activeViewChanged.connect( self._onActiveViewChanged) # The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice new_node = CuraSceneNode(no_setting_override=True) new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) # Force garbage collection. # For some reason, Python has a tendency to keep the layer data # in memory longer than needed. Forcing the GC to run here makes # sure any old layer data is really cleaned up before adding new. gc.collect() mesh = MeshData() layer_data = LayerDataBuilder.LayerDataBuilder() layer_count = len(self._layers) # Find the minimum layer number # When disabling the remove empty first layers setting, the minimum layer number will be a positive # value. In that case the first empty layers will be discarded and start processing layers from the # first layer with data. # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # simply offset all other layers so the lowest layer is always 0. It could happens that the first # raft layer has value -8 but there are just 4 raft (negative) layers. min_layer_number = sys.maxsize negative_layers = 0 for layer in self._layers: if layer.repeatedMessageCount("path_segment") > 0: if layer.id < min_layer_number: min_layer_number = layer.id if layer.id < 0: negative_layers += 1 current_layer = 0 for layer in self._layers: # If the layer is below the minimum, it means that there is no data, so that we don't create a layer # data. However, if there are empty layers in between, we compute them. if layer.id < min_layer_number: continue # Layers are offset by the minimum layer number. In case the raft (negative layers) is being used, # then the absolute layer number is adjusted by removing the empty layers that can be in between raft # and the model abs_layer_number = layer.id - min_layer_number if layer.id >= 0 and negative_layers != 0: abs_layer_number += (min_layer_number + negative_layers) layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) layer_data.setLayerHeight(abs_layer_number, layer.height) layer_data.setLayerThickness(abs_layer_number, layer.thickness) for p in range(layer.repeatedMessageCount("path_segment")): polygon = layer.getRepeatedMessage("path_segment", p) extruder = polygon.extruder line_types = numpy.fromstring( polygon.line_type, dtype="u1") # Convert bytearray to numpy array line_types = line_types.reshape((-1, 1)) points = numpy.fromstring( polygon.points, dtype="f4") # Convert bytearray to numpy array if polygon.point_type == 0: # Point2D points = points.reshape( (-1, 2) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. else: # Point3D points = points.reshape((-1, 3)) line_widths = numpy.fromstring( polygon.line_width, dtype="f4") # Convert bytearray to numpy array line_widths = line_widths.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_thicknesses = numpy.fromstring( polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array line_thicknesses = line_thicknesses.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. line_feedrates = numpy.fromstring( polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array line_feedrates = line_feedrates.reshape( (-1, 1) ) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly. # Create a new 3D-array, copy the 2D points over and insert the right height. # This uses manual array creation + copy rather than numpy.insert since this is # faster. new_points = numpy.empty((len(points), 3), numpy.float32) if polygon.point_type == 0: # Point2D new_points[:, 0] = points[:, 0] new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation new_points[:, 2] = -points[:, 1] else: # Point3D new_points[:, 0] = points[:, 0] new_points[:, 1] = points[:, 2] new_points[:, 2] = -points[:, 1] this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates) this_poly.buildCache() this_layer.polygons.append(this_poly) Job.yieldThread() Job.yieldThread() current_layer += 1 progress = (current_layer / layer_count) * 99 # TODO: Rebuild the layer data mesh once the layer has been processed. # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh. if self._abort_requested: if self._progress_message: self._progress_message.hide() return if self._progress_message: self._progress_message.setProgress(progress) # We are done processing all the layers we got from the engine, now create a mesh out of the data # Find out colors per extruder global_container_stack = Application.getInstance( ).getGlobalContainerStack() manager = ExtruderManager.getInstance() extruders = manager.getActiveExtruderStacks() if extruders: material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) for extruder in extruders: position = int( extruder.getMetaDataEntry("position", default="0")) try: default_color = ExtrudersModel.defaultColors[position] except IndexError: default_color = "#e0e000" color_code = extruder.material.getMetaDataEntry( "color_code", default=default_color) color = colorCodeToRGBA(color_code) material_color_map[position, :] = color else: # Single extruder via global stack. material_color_map = numpy.zeros((1, 4), dtype=numpy.float32) color_code = global_container_stack.material.getMetaDataEntry( "color_code", default="#e0e000") color = colorCodeToRGBA(color_code) material_color_map[0, :] = color # We have to scale the colors for compatibility mode if OpenGLContext.isLegacyOpenGL() or bool( Application.getInstance().getPreferences().getValue( "view/force_layer_view_compatibility_mode")): line_type_brightness = 0.5 # for compatibility mode else: line_type_brightness = 1.0 layer_mesh = layer_data.build(material_color_map, line_type_brightness) if self._abort_requested: if self._progress_message: self._progress_message.hide() return # Add LayerDataDecorator to scene node to indicate that the node has layer data decorator = LayerDataDecorator.LayerDataDecorator() decorator.setLayerData(layer_mesh) new_node.addDecorator(decorator) new_node.setMeshData(mesh) # Set build volume as parent, the build volume can move as a result of raft settings. # It makes sense to set the build volume as parent: the print is actually printed on it. new_node_parent = Application.getInstance().getBuildVolume() new_node.setParent( new_node_parent) # Note: After this we can no longer abort! settings = Application.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): new_node.setPosition( Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2)) if self._progress_message: self._progress_message.setProgress(100) if self._progress_message: self._progress_message.hide() # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed. self._layers = None Logger.log("d", "Processing layers took %s seconds", time() - start_time) def _onActiveViewChanged(self): if self.isRunning(): if Application.getInstance().getController().getActiveView( ).getPluginId() == "SimulationView": if not self._progress_message: self._progress_message = Message( catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0, catalog.i18nc("@info:title", "Information")) if self._progress_message.getProgress() != 100: self._progress_message.show() else: if self._progress_message: self._progress_message.hide()
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): def __init__(self, device_id, address: str, properties, parent = None) -> None: super().__init__(device_id = device_id, address = address, properties = properties, connection_type = ConnectionType.NetworkConnection, parent = parent) self._api_prefix = "/api/v1/" self._number_of_extruders = 2 self._authentication_id = None self._authentication_key = None self._authentication_counter = 0 self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min) self._authentication_timer = QTimer() self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval self._authentication_timer.setSingleShot(False) self._authentication_timer.timeout.connect(self._onAuthenticationTimer) # The messages are created when connect is called the first time. # This ensures that the messages are only created for devices that actually want to connect. self._authentication_requested_message = None self._authentication_failed_message = None self._authentication_succeeded_message = None self._not_authenticated_message = None self.authenticationStateChanged.connect(self._onAuthenticationStateChanged) self.setPriority(3) # Make sure the output device gets selected above local file output self.setName(self._id) self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network")) self.setIconName("print") self._output_controller = LegacyUM3PrinterOutputController(self) def _createMonitorViewFromQML(self) -> None: if self._monitor_view_qml_path is None and PluginRegistry.getInstance() is not None: self._monitor_view_qml_path = os.path.join( PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "resources", "qml", "MonitorStage.qml" ) super()._createMonitorViewFromQML() def _onAuthenticationStateChanged(self): # We only accept commands if we are authenticated. self._setAcceptsCommands(self._authentication_state == AuthState.Authenticated) if self._authentication_state == AuthState.Authenticated: self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network.")) elif self._authentication_state == AuthState.AuthenticationRequested: self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. Please approve the access request on the printer.")) elif self._authentication_state == AuthState.AuthenticationDenied: self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. No access to control the printer.")) def _setupMessages(self): self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status", "Access to the printer requested. Please approve the request on the printer"), lifetime=0, dismissable=False, progress=0, title=i18n_catalog.i18nc("@info:title", "Authentication status")) self._authentication_failed_message = Message("", title=i18n_catalog.i18nc("@info:title", "Authentication Status")) self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None, i18n_catalog.i18nc("@info:tooltip", "Re-send the access request")) self._authentication_failed_message.actionTriggered.connect(self._messageCallback) self._authentication_succeeded_message = Message( i18n_catalog.i18nc("@info:status", "Access to the printer accepted"), title=i18n_catalog.i18nc("@info:title", "Authentication Status")) self._not_authenticated_message = Message( i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."), title=i18n_catalog.i18nc("@info:title", "Authentication Status")) self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"), None, i18n_catalog.i18nc("@info:tooltip", "Send access request to the printer")) self._not_authenticated_message.actionTriggered.connect(self._messageCallback) def _messageCallback(self, message_id=None, action_id="Retry"): if action_id == "Request" or action_id == "Retry": if self._authentication_failed_message: self._authentication_failed_message.hide() if self._not_authenticated_message: self._not_authenticated_message.hide() self._requestAuthentication() def connect(self): super().connect() self._setupMessages() global_container = CuraApplication.getInstance().getGlobalContainerStack() if global_container: self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None) self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None) def close(self): super().close() if self._authentication_requested_message: self._authentication_requested_message.hide() if self._authentication_failed_message: self._authentication_failed_message.hide() if self._authentication_succeeded_message: self._authentication_succeeded_message.hide() self._sending_gcode = False self._compressing_gcode = False self._authentication_timer.stop() ## Send all material profiles to the printer. def _sendMaterialProfiles(self): Logger.log("i", "Sending material profiles to printer") # TODO: Might want to move this to a job... for container in ContainerRegistry.getInstance().findInstanceContainers(type="material"): try: xml_data = container.serialize() if xml_data == "" or xml_data is None: continue names = ContainerManager.getInstance().getLinkedMaterials(container.getId()) if names: # There are other materials that share this GUID. if not container.isReadOnly(): continue # If it's not readonly, it's created by user, so skip it. file_name = "none.xml" self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), on_finished=None) except NotImplementedError: # If the material container is not the most "generic" one it can't be serialized an will raise a # NotImplementedError. We can simply ignore these. pass def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: if not self.activePrinter: # No active printer. Unable to write return if self.activePrinter.state not in ["idle", ""]: # Printer is not able to accept commands. return if self._authentication_state != AuthState.Authenticated: # Not authenticated, so unable to send job. return self.writeStarted.emit(self) gcode_dict = getattr(CuraApplication.getInstance().getController().getScene(), "gcode_dict", []) active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate gcode_list = gcode_dict[active_build_plate_id] if not gcode_list: # Unable to find g-code. Nothing to send return self._gcode = gcode_list errors = self._checkForErrors() if errors: text = i18n_catalog.i18nc("@label", "Unable to start a new print job.") informative_text = i18n_catalog.i18nc("@label", "There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. " "Please resolve this issues before continuing.") detailed_text = "" for error in errors: detailed_text += error + "\n" CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), text, informative_text, detailed_text, buttons=QMessageBox.Ok, icon=QMessageBox.Critical, callback = self._messageBoxCallback ) return # Don't continue; Errors must block sending the job to the printer. # There might be multiple things wrong with the configuration. Check these before starting. warnings = self._checkForWarnings() if warnings: text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?") informative_text = i18n_catalog.i18nc("@label", "There is a mismatch between the configuration or calibration of the printer and Cura. " "For the best result, always slice for the PrintCores and materials that are inserted in your printer.") detailed_text = "" for warning in warnings: detailed_text += warning + "\n" CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), text, informative_text, detailed_text, buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, callback=self._messageBoxCallback ) return # No warnings or errors, so we're good to go. self._startPrint() # Notify the UI that a switch to the print monitor should happen CuraApplication.getInstance().getController().setActiveStage("MonitorStage") def _startPrint(self): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: 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._sending_gcode = True self._send_gcode_start = time() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1, i18n_catalog.i18nc("@info:title", "Sending Data")) self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "") self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered) self._progress_message.show() compressed_gcode = self._compressGCode() if compressed_gcode is None: # Abort was called. return file_name = "%s.gcode.gz" % CuraApplication.getInstance().getPrintInformation().jobName self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode, on_finished=self._onPostPrintJobFinished) return def _progressMessageActionTriggered(self, message_id=None, action_id=None): if action_id == "Abort": Logger.log("d", "User aborted sending print to remote.") self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False CuraApplication.getInstance().getController().setActiveStage("PrepareStage") def _onPostPrintJobFinished(self, reply): self._progress_message.hide() self._sending_gcode = False def _onUploadPrintJobProgress(self, bytes_sent, bytes_total): if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() if new_progress > self._progress_message.getProgress(): self._progress_message.show() # Ensure that the message is visible. self._progress_message.setProgress(bytes_sent / bytes_total * 100) else: self._progress_message.setProgress(0) self._progress_message.hide() def _messageBoxCallback(self, button): def delayedCallback(): if button == QMessageBox.Yes: self._startPrint() else: CuraApplication.getInstance().getController().setActiveStage("PrepareStage") # For some unknown reason Cura on OSX will hang if we do the call back code # immediately without first returning and leaving QML's event system. QTimer.singleShot(100, delayedCallback) def _checkForErrors(self): errors = [] print_information = CuraApplication.getInstance().getPrintInformation() if not print_information.materialLengths: Logger.log("w", "There is no material length information. Unable to check for errors.") return errors for index, extruder in enumerate(self.activePrinter.extruders): # Due to airflow issues, both slots must be loaded, regardless if they are actually used or not. if extruder.hotendID == "": # No Printcore loaded. errors.append(i18n_catalog.i18nc("@info:status", "No Printcore loaded in slot {slot_number}".format(slot_number=index + 1))) if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0: # The extruder is by this print. if extruder.activeMaterial is None: # No active material errors.append(i18n_catalog.i18nc("@info:status", "No material loaded in slot {slot_number}".format(slot_number=index + 1))) return errors def _checkForWarnings(self): warnings = [] print_information = CuraApplication.getInstance().getPrintInformation() if not print_information.materialLengths: Logger.log("w", "There is no material length information. Unable to check for warnings.") return warnings extruder_manager = ExtruderManager.getInstance() for index, extruder in enumerate(self.activePrinter.extruders): if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0: # The extruder is by this print. # TODO: material length check # Check if the right Printcore is active. variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"}) if variant: if variant.getName() != extruder.hotendID: warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}".format(cura_printcore_name = variant.getName(), remote_printcore_name = extruder.hotendID, extruder_id = index + 1))) else: Logger.log("w", "Unable to find variant.") # Check if the right material is loaded. local_material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"}) if local_material: if extruder.activeMaterial.guid != local_material.getMetaDataEntry("GUID"): Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID")) warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(local_material.getName(), extruder.activeMaterial.name, index + 1)) else: Logger.log("w", "Unable to find material.") return warnings def _update(self): if not super()._update(): return if self._authentication_state == AuthState.NotAuthenticated: if self._authentication_id is None and self._authentication_key is None: # This machine doesn't have any authentication, so request it. self._requestAuthentication() elif self._authentication_id is not None and self._authentication_key is not None: # We have authentication info, but we haven't checked it out yet. Do so now. self._verifyAuthentication() elif self._authentication_state == AuthState.AuthenticationReceived: # We have an authentication, but it's not confirmed yet. self._checkAuthentication() # We don't need authentication for requesting info, so we can go right ahead with requesting this. self.get("printer", on_finished=self._onGetPrinterDataFinished) self.get("print_job", on_finished=self._onGetPrintJobFinished) def _resetAuthenticationRequestedMessage(self): if self._authentication_requested_message: self._authentication_requested_message.hide() self._authentication_timer.stop() self._authentication_counter = 0 def _onAuthenticationTimer(self): self._authentication_counter += 1 self._authentication_requested_message.setProgress( self._authentication_counter / self._max_authentication_counter * 100) if self._authentication_counter > self._max_authentication_counter: self._authentication_timer.stop() Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id) self.setAuthenticationState(AuthState.AuthenticationDenied) self._resetAuthenticationRequestedMessage() self._authentication_failed_message.show() def _verifyAuthentication(self): Logger.log("d", "Attempting to verify authentication") # This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator. self.get("auth/verify", on_finished=self._onVerifyAuthenticationCompleted) def _onVerifyAuthenticationCompleted(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 401: # Something went wrong; We somehow tried to verify authentication without having one. Logger.log("d", "Attempted to verify auth without having one.") self._authentication_id = None self._authentication_key = None self.setAuthenticationState(AuthState.NotAuthenticated) elif status_code == 403 and self._authentication_state != AuthState.Authenticated: # If we were already authenticated, we probably got an older message back all of the sudden. Drop that. Logger.log("d", "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ", self._authentication_state) self.setAuthenticationState(AuthState.AuthenticationDenied) self._authentication_failed_message.show() elif status_code == 200: self.setAuthenticationState(AuthState.Authenticated) def _checkAuthentication(self): Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) self.get("auth/check/" + str(self._authentication_id), on_finished=self._onCheckAuthenticationFinished) def _onCheckAuthenticationFinished(self, reply): if str(self._authentication_id) not in reply.url().toString(): Logger.log("w", "Got an old id response.") # Got response for old authentication ID. return try: data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.") return if data.get("message", "") == "authorized": Logger.log("i", "Authentication was approved") self.setAuthenticationState(AuthState.Authenticated) self._saveAuthentication() # Double check that everything went well. self._verifyAuthentication() # Notify the user. self._resetAuthenticationRequestedMessage() self._authentication_succeeded_message.show() elif data.get("message", "") == "unauthorized": Logger.log("i", "Authentication was denied.") self.setAuthenticationState(AuthState.AuthenticationDenied) self._authentication_failed_message.show() def _saveAuthentication(self) -> None: global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() if self._authentication_key is None: Logger.log("e", "Authentication key is None, nothing to save.") return if self._authentication_id is None: Logger.log("e", "Authentication id is None, nothing to save.") return if global_container_stack: global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key) global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id) # Force save so we are sure the data is not lost. CuraApplication.getInstance().saveStack(global_container_stack) Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) else: Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) def _onRequestAuthenticationFinished(self, reply): try: data = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.") self.setAuthenticationState(AuthState.NotAuthenticated) return self.setAuthenticationState(AuthState.AuthenticationReceived) self._authentication_id = data["id"] self._authentication_key = data["key"] Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.", self._authentication_id, self._getSafeAuthKey()) def _requestAuthentication(self): self._authentication_requested_message.show() self._authentication_timer.start() # Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might # give issues. self._authentication_key = None self._authentication_id = None self.post("auth/request", json.dumps({"application": "Cura-" + CuraApplication.getInstance().getVersion(), "user": self._getUserName()}), on_finished=self._onRequestAuthenticationFinished) self.setAuthenticationState(AuthState.AuthenticationRequested) def _onAuthenticationRequired(self, reply, authenticator): if self._authentication_id is not None and self._authentication_key is not None: Logger.log("d", "Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s", self._id, self._authentication_id, self._getSafeAuthKey()) authenticator.setUser(self._authentication_id) authenticator.setPassword(self._authentication_key) else: Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._id) def _onGetPrintJobFinished(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if not self._printers: return # Ignore the data for now, we don't have info about a printer yet. printer = self._printers[0] if status_code == 200: try: result = 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 if printer.activePrintJob is None: print_job = PrintJobOutputModel(output_controller=self._output_controller) printer.updateActivePrintJob(print_job) else: print_job = printer.activePrintJob print_job.updateState(result["state"]) print_job.updateTimeElapsed(result["time_elapsed"]) print_job.updateTimeTotal(result["time_total"]) print_job.updateName(result["name"]) elif status_code == 404: # No job found, so delete the active print job (if any!) printer.updateActivePrintJob(None) else: Logger.log("w", "Got status code {status_code} while trying to get printer data".format(status_code=status_code)) def materialHotendChangedMessage(self, callback): CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"), i18n_catalog.i18nc("@label", "Would you like to use your current printer configuration in Cura?"), i18n_catalog.i18nc("@label", "The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."), buttons=QMessageBox.Yes + QMessageBox.No, icon=QMessageBox.Question, callback=callback ) def _onGetPrinterDataFinished(self, reply): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 200: try: result = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.log("w", "Received an invalid printer state message: Not valid JSON.") return if not self._printers: # Quickest way to get the firmware version is to grab it from the zeroconf. firmware_version = self._properties.get(b"firmware_version", b"").decode("utf-8") self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version)] self._printers[0].setCameraUrl(QUrl("http://" + self._address + ":8080/?action=stream")) for extruder in self._printers[0].extruders: extruder.activeMaterialChanged.connect(self.materialIdChanged) extruder.hotendIDChanged.connect(self.hotendIdChanged) self.printersChanged.emit() # LegacyUM3 always has a single printer. printer = self._printers[0] printer.updateBedTemperature(result["bed"]["temperature"]["current"]) printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"]) printer.updateState(result["status"]) try: # If we're still handling the request, we should ignore remote for a bit. if not printer.getController().isPreheatRequestInProgress(): printer.updateIsPreheating(result["bed"]["pre_heat"]["active"]) except KeyError: # Older firmwares don't support preheating, so we need to fake it. pass head_position = result["heads"][0]["position"] printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"]) for index in range(0, self._number_of_extruders): temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"] extruder = printer.extruders[index] extruder.updateTargetHotendTemperature(temperatures["target"]) extruder.updateHotendTemperature(temperatures["current"]) material_guid = result["heads"][0]["extruders"][index]["active_material"]["guid"] if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid: # Find matching material (as we need to set brand, type & color) containers = ContainerRegistry.getInstance().findInstanceContainers(type="material", GUID=material_guid) if containers: color = containers[0].getMetaDataEntry("color_code") brand = containers[0].getMetaDataEntry("brand") material_type = containers[0].getMetaDataEntry("material") name = containers[0].getName() else: # Unknown material. color = "#00000000" brand = "Unknown" material_type = "Unknown" name = "Unknown" material = MaterialOutputModel(guid=material_guid, type=material_type, brand=brand, color=color, name = name) extruder.updateActiveMaterial(material) try: hotend_id = result["heads"][0]["extruders"][index]["hotend"]["id"] except KeyError: hotend_id = "" printer.extruders[index].updateHotendID(hotend_id) else: Logger.log("w", "Got status code {status_code} while trying to get printer data".format(status_code = status_code)) ## Convenience function to "blur" out all but the last 5 characters of the auth key. # This can be used to debug print the key, without it compromising the security. def _getSafeAuthKey(self): if self._authentication_key is not None: result = self._authentication_key[-5:] result = "********" + result return result return self._authentication_key
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): printJobsChanged = pyqtSignal() activePrinterChanged = pyqtSignal() activeCameraUrlChanged = pyqtSignal() receivedPrintJobsChanged = pyqtSignal() # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in. # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions. clusterPrintersChanged = pyqtSignal() def __init__(self, device_id, address, properties, parent=None) -> None: super().__init__(device_id=device_id, address=address, properties=properties, parent=parent) self._api_prefix = "/cluster-api/v1/" self._number_of_extruders = 2 self._dummy_lambdas = ( "", {}, io.BytesIO() ) #type: Tuple[str, Dict, Union[io.StringIO, io.BytesIO]] self._print_jobs = [] # type: List[UM3PrintJobOutputModel] self._received_print_jobs = False # type: bool self._monitor_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "../resources/qml/ClusterMonitorItem.qml") self._control_view_qml_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "../resources/qml/ClusterControlItem.qml") # See comments about this hack with the clusterPrintersChanged signal self.printersChanged.connect(self.clusterPrintersChanged) self._accepts_commands = True # type: bool # Cluster does not have authentication, so default to authenticated self._authentication_state = AuthState.Authenticated self._error_message = None # type: Optional[Message] self._write_job_progress_message = None # type: Optional[Message] self._progress_message = None # type: Optional[Message] self._active_printer = None # type: Optional[PrinterOutputModel] self._printer_selection_dialog = None # type: QObject self.setPriority( 3 ) # Make sure the output device gets selected above local file output self.setName(self._id) self.setShortDescription( i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) self.setDescription( i18n_catalog.i18nc("@properties:tooltip", "Print over network")) self.setConnectionText( i18n_catalog.i18nc("@info:status", "Connected over the network")) self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str] self._finished_jobs = [] # type: List[UM3PrintJobOutputModel] self._cluster_size = int(properties.get(b"cluster_size", 0)) # type: int self._latest_reply_handler = None # type: Optional[QNetworkReply] self._sending_job = None self._active_camera_url = QUrl() # type: QUrl def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: self.writeStarted.emit(self) self.sendMaterialProfiles() # Formats supported by this application (file types that we can actually write). if file_handler: file_formats = file_handler.getSupportedFileTypesWrite() else: file_formats = CuraApplication.getInstance().getMeshFileHandler( ).getSupportedFileTypesWrite() global_stack = CuraApplication.getInstance().getGlobalContainerStack() # Create a list from the supported file formats string. if not global_stack: Logger.log("e", "Missing global stack!") return machine_file_formats = global_stack.getMetaDataEntry( "file_formats").split(";") machine_file_formats = [ file_type.strip() for file_type in machine_file_formats ] # Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format. if "application/x-ufp" not in machine_file_formats and Version( self.firmwareVersion) >= Version("4.4"): machine_file_formats = ["application/x-ufp"] + machine_file_formats # Take the intersection between file_formats and machine_file_formats. format_by_mimetype = { format["mime_type"]: format for format in file_formats } file_formats = [ format_by_mimetype[mimetype] for mimetype in machine_file_formats ] #Keep them ordered according to the preference in machine_file_formats. if len(file_formats) == 0: Logger.log("e", "There are no file formats available to write with!") raise OutputDeviceError.WriteRequestFailedError( i18n_catalog.i18nc( "@info:status", "There are no file formats available to write with!")) preferred_format = file_formats[0] # Just take the first file format available. if file_handler is not None: writer = file_handler.getWriterByMimeType( cast(str, preferred_format["mime_type"])) else: writer = CuraApplication.getInstance().getMeshFileHandler( ).getWriterByMimeType(cast(str, preferred_format["mime_type"])) if not writer: Logger.log("e", "Unexpected error when trying to get the FileWriter") return # This function pauses with the yield, waiting on instructions on which printer it needs to print with. if not writer: Logger.log("e", "Missing file or mesh writer!") return self._sending_job = self._sendPrintJob(writer, preferred_format, nodes) if self._sending_job is not None: self._sending_job.send(None) # Start the generator. if len(self._printers) > 1: # We need to ask the user. self._spawnPrinterSelectionDialog() is_job_sent = True else: # Just immediately continue. self._sending_job.send("") # No specifically selected printer. is_job_sent = self._sending_job.send(None) def _spawnPrinterSelectionDialog(self): if self._printer_selection_dialog is None: path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/PrintWindow.qml") self._printer_selection_dialog = CuraApplication.getInstance( ).createQmlComponent(path, {"OutputDevice": self}) if self._printer_selection_dialog is not None: self._printer_selection_dialog.show() @pyqtProperty(int, constant=True) def clusterSize(self) -> int: return self._cluster_size ## Allows the user to choose a printer to print with from the printer # selection dialogue. # \param target_printer The name of the printer to target. @pyqtSlot(str) def selectPrinter(self, target_printer: str = "") -> None: if self._sending_job is not None: self._sending_job.send(target_printer) @pyqtSlot() def cancelPrintSelection(self) -> None: self._sending_gcode = False ## Greenlet to send a job to the printer over the network. # # This greenlet gets called asynchronously in requestWrite. It is a # greenlet in order to optionally wait for selectPrinter() to select a # printer. # The greenlet yields exactly three times: First time None, # \param writer The file writer to use to create the data. # \param preferred_format A dictionary containing some information about # what format to write to. This is necessary to create the correct buffer # types and file extension and such. def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict, nodes: List[SceneNode]): Logger.log("i", "Sending print job to printer.") if self._sending_gcode: self._error_message = Message( i18n_catalog.i18nc( "@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job." )) self._error_message.show() yield #Wait on the user to select a target printer. yield #Wait for the write job to be finished. yield False #Return whether this was a success or not. yield #Prevent StopIteration. self._sending_gcode = True target_printer = yield #Potentially wait on the user to select a target printer. # Using buffering greatly reduces the write time for many lines of gcode stream = io.BytesIO( ) # type: Union[io.BytesIO, io.StringIO]# Binary mode. if preferred_format["mode"] == FileWriter.OutputMode.TextMode: stream = io.StringIO() job = WriteFileJob(writer, stream, nodes, preferred_format["mode"]) self._write_job_progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime=0, dismissable=False, progress=-1, title=i18n_catalog.i18nc("@info:title", "Sending Data"), use_inactivity_timer=False) self._write_job_progress_message.show() self._dummy_lambdas = (target_printer, preferred_format, stream) job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished) job.start() yield True # Return that we had success! yield # To prevent having to catch the StopIteration exception. def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None: if self._write_job_progress_message: self._write_job_progress_message.hide() self._progress_message = Message( i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime=0, dismissable=False, progress=-1, title=i18n_catalog.i18nc("@info:title", "Sending Data")) self._progress_message.addAction("Abort", i18n_catalog.i18nc( "@action:button", "Cancel"), icon=None, description="") self._progress_message.actionTriggered.connect( self._progressMessageActionTriggered) self._progress_message.show() parts = [] target_printer, preferred_format, stream = self._dummy_lambdas # If a specific printer was selected, it should be printed with that machine. if target_printer: target_printer = self._printer_uuid_to_unique_name_mapping[ target_printer] parts.append( self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain")) # Add user name to the print_job parts.append( self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) file_name = CuraApplication.getInstance().getPrintInformation( ).jobName + "." + preferred_format["extension"] output = stream.getvalue( ) # Either str or bytes depending on the output mode. if isinstance(stream, io.StringIO): output = cast(str, output).encode("utf-8") output = cast(bytes, output) parts.append( self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output)) self._latest_reply_handler = self.postFormWithParts( "print_jobs/", parts, on_finished=self._onPostPrintJobFinished, on_progress=self._onUploadPrintJobProgress) @pyqtProperty(QObject, notify=activePrinterChanged) def activePrinter(self) -> Optional[PrinterOutputModel]: return self._active_printer @pyqtSlot(QObject) def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None: if self._active_printer != printer: self._active_printer = printer self.activePrinterChanged.emit() @pyqtProperty(QUrl, notify=activeCameraUrlChanged) def activeCameraUrl(self) -> "QUrl": return self._active_camera_url @pyqtSlot(QUrl) def setActiveCameraUrl(self, camera_url: "QUrl") -> None: if self._active_camera_url != camera_url: self._active_camera_url = camera_url self.activeCameraUrlChanged.emit() def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None: if self._progress_message: self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None: if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() if self._progress_message and 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) # If successfully sent: if bytes_sent == bytes_total: # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to # the monitor tab. self._success_message = Message(i18n_catalog.i18nc( "@info:status", "Print job was successfully sent to the printer."), lifetime=5, dismissable=True, title=i18n_catalog.i18nc( "@info:title", "Data Sent")) self._success_message.addAction("View", i18n_catalog.i18nc( "@action:button", "View in Monitor"), icon=None, description="") self._success_message.actionTriggered.connect( self._successMessageActionTriggered) self._success_message.show() else: if self._progress_message is not None: self._progress_message.setProgress(0) self._progress_message.hide() def _progressMessageActionTriggered( self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: if action_id == "Abort": Logger.log("d", "User aborted sending print to remote.") if self._progress_message is not None: self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False CuraApplication.getInstance().getController().setActiveStage( "PrepareStage") # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request # the "reply" should be disconnected if self._latest_reply_handler: self._latest_reply_handler.disconnect() self._latest_reply_handler = None def _successMessageActionTriggered( self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: if action_id == "View": CuraApplication.getInstance().getController().setActiveStage( "MonitorStage") @pyqtSlot() def openPrintJobControlPanel(self) -> None: Logger.log("d", "Opening print job control panel...") QDesktopServices.openUrl( QUrl("http://" + self._address + "/print_jobs")) @pyqtSlot() def openPrinterControlPanel(self) -> None: Logger.log("d", "Opening printer control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers")) @pyqtProperty("QVariantList", notify=printJobsChanged) def printJobs(self) -> List[UM3PrintJobOutputModel]: return self._print_jobs @pyqtProperty(bool, notify=receivedPrintJobsChanged) def receivedPrintJobs(self) -> bool: return self._received_print_jobs @pyqtProperty("QVariantList", notify=printJobsChanged) def queuedPrintJobs(self) -> List[UM3PrintJobOutputModel]: return [ print_job for print_job in self._print_jobs if print_job.state == "queued" or print_job.state == "error" ] @pyqtProperty("QVariantList", notify=printJobsChanged) def activePrintJobs(self) -> List[UM3PrintJobOutputModel]: return [ print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued" ] @pyqtProperty("QVariantList", notify=clusterPrintersChanged) def connectedPrintersTypeCount(self) -> List[Dict[str, str]]: printer_count = {} # type: Dict[str, int] for printer in self._printers: if printer.type in printer_count: printer_count[printer.type] += 1 else: printer_count[printer.type] = 1 result = [] for machine_type in printer_count: result.append({ "machine_type": machine_type, "count": str(printer_count[machine_type]) }) return result @pyqtProperty("QVariantList", notify=clusterPrintersChanged) def printers(self): return self._printers @pyqtSlot(int, result=str) def formatDuration(self, seconds: int) -> str: return Duration(seconds).getDisplayString(DurationFormat.Format.Short) @pyqtSlot(int, result=str) def getTimeCompleted(self, time_remaining: int) -> str: current_time = time() datetime_completed = 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: int) -> str: current_time = time() datetime_completed = datetime.fromtimestamp(current_time + time_remaining) return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper() @pyqtSlot(str) def sendJobToTop(self, print_job_uuid: str) -> None: # This function is part of the output device (and not of the printjob output model) as this type of operation # is a modification of the cluster queue and not of the actual job. data = "{\"to_position\": 0}" self.put( "print_jobs/{uuid}/move_to_position".format(uuid=print_job_uuid), data, on_finished=None) @pyqtSlot(str) def deleteJobFromQueue(self, print_job_uuid: str) -> None: # This function is part of the output device (and not of the printjob output model) as this type of operation # is a modification of the cluster queue and not of the actual job. self.delete("print_jobs/{uuid}".format(uuid=print_job_uuid), on_finished=None) @pyqtSlot(str) def forceSendJob(self, print_job_uuid: str) -> None: data = "{\"force\": true}" self.put("print_jobs/{uuid}".format(uuid=print_job_uuid), data, on_finished=None) def _printJobStateChanged(self) -> None: username = self._getUserName() if username is None: return # We only want to show notifications if username is set. finished_jobs = [ job for job in self._print_jobs if job.state == "wait_cleanup" ] newly_finished_jobs = [ job for job in finished_jobs if job not in self._finished_jobs and job.owner == username ] for job in newly_finished_jobs: if job.assignedPrinter: job_completed_text = i18n_catalog.i18nc( "@info:status", "Printer '{printer_name}' has finished printing '{job_name}'." .format(printer_name=job.assignedPrinter.name, job_name=job.name)) else: job_completed_text = i18n_catalog.i18nc( "@info:status", "The print job '{job_name}' was finished.".format( job_name=job.name)) job_completed_message = Message(text=job_completed_text, title=i18n_catalog.i18nc( "@info:status", "Print finished")) job_completed_message.show() # Ensure UI gets updated self.printJobsChanged.emit() # Keep a list of all completed jobs so we know if something changed next time. self._finished_jobs = finished_jobs ## Called when the connection to the cluster changes. def connect(self) -> None: super().connect() self.sendMaterialProfiles() def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None: reply_url = reply.url().toString() uuid = reply_url[reply_url.find("print_jobs/") + len("print_jobs/"):reply_url.rfind("/preview_image")] print_job = findByKey(self._print_jobs, uuid) if print_job: image = QImage() image.loadFromData(reply.readAll()) print_job.updatePreviewImage(image) def _update(self) -> None: super()._update() self.get("printers/", on_finished=self._onGetPrintersDataFinished) self.get("print_jobs/", on_finished=self._onGetPrintJobsFinished) for print_job in self._print_jobs: if print_job.getPreviewImage() is None: self.get("print_jobs/{uuid}/preview_image".format( uuid=print_job.key), on_finished=self._onGetPreviewImageFinished) def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None: self._received_print_jobs = True self.receivedPrintJobsChanged.emit() if not checkValidGetReply(reply): return result = loadJsonFromReply(reply) if result is None: return print_jobs_seen = [] job_list_changed = False for idx, print_job_data in enumerate(result): print_job = findByKey(self._print_jobs, print_job_data["uuid"]) if print_job is None: print_job = self._createPrintJobModel(print_job_data) job_list_changed = True elif not job_list_changed: # Check if the order of the jobs has changed since the last check if self._print_jobs.index(print_job) != idx: job_list_changed = True self._updatePrintJob(print_job, print_job_data) if print_job.state != "queued" and print_job.state != "error": # Print job should be assigned to a printer. if print_job.state in [ "failed", "finished", "aborted", "none" ]: # Print job was already completed, so don't attach it to a printer. printer = None else: printer = self._getPrinterByKey( print_job_data["printer_uuid"]) else: # The job can "reserve" a printer if some changes are required. printer = self._getPrinterByKey(print_job_data["assigned_to"]) if printer: printer.updateActivePrintJob(print_job) print_jobs_seen.append(print_job) # Check what jobs need to be removed. removed_jobs = [ print_job for print_job in self._print_jobs if print_job not in print_jobs_seen ] for removed_job in removed_jobs: job_list_changed = job_list_changed or self._removeJob(removed_job) if job_list_changed: # Override the old list with the new list (either because jobs were removed / added or order changed) self._print_jobs = print_jobs_seen self.printJobsChanged.emit( ) # Do a single emit for all print job changes. def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None: if not checkValidGetReply(reply): return result = loadJsonFromReply(reply) if result is None: return printer_list_changed = False printers_seen = [] for printer_data in result: printer = findByKey(self._printers, printer_data["uuid"]) if printer is None: printer = self._createPrinterModel(printer_data) printer_list_changed = True printers_seen.append(printer) self._updatePrinter(printer, printer_data) removed_printers = [ printer for printer in self._printers if printer not in printers_seen ] for printer in removed_printers: self._removePrinter(printer) if removed_printers or printer_list_changed: self.printersChanged.emit() def _createPrinterModel(self, data: Dict[str, Any]) -> PrinterOutputModel: printer = PrinterOutputModel( output_controller=ClusterUM3PrinterOutputController(self), number_of_extruders=self._number_of_extruders) printer.setCameraUrl( QUrl("http://" + data["ip_address"] + ":8080/?action=stream")) self._printers.append(printer) return printer def _createPrintJobModel(self, data: Dict[str, Any]) -> UM3PrintJobOutputModel: print_job = UM3PrintJobOutputModel( output_controller=ClusterUM3PrinterOutputController(self), key=data["uuid"], name=data["name"]) configuration = ConfigurationModel() extruders = [ ExtruderConfigurationModel(position=idx) for idx in range(0, self._number_of_extruders) ] for index in range(0, self._number_of_extruders): try: extruder_data = data["configuration"][index] except IndexError: continue extruder = extruders[int( data["configuration"][index]["extruder_index"])] extruder.setHotendID(extruder_data.get("print_core_id", "")) extruder.setMaterial( self._createMaterialOutputModel( extruder_data.get("material", {}))) configuration.setExtruderConfigurations(extruders) print_job.updateConfiguration(configuration) print_job.setCompatibleMachineFamilies( data.get("compatible_machine_families", [])) print_job.stateChanged.connect(self._printJobStateChanged) return print_job def _updatePrintJob(self, print_job: UM3PrintJobOutputModel, data: Dict[str, Any]) -> None: print_job.updateTimeTotal(data["time_total"]) print_job.updateTimeElapsed(data["time_elapsed"]) impediments_to_printing = data.get("impediments_to_printing", []) print_job.updateOwner(data["owner"]) status_set_by_impediment = False for impediment in impediments_to_printing: if impediment["severity"] == "UNFIXABLE": status_set_by_impediment = True print_job.updateState("error") break if not status_set_by_impediment: print_job.updateState(data["status"]) print_job.updateConfigurationChanges( self._createConfigurationChanges( data["configuration_changes_required"])) def _createConfigurationChanges( self, data: List[Dict[str, Any]]) -> List[ConfigurationChangeModel]: result = [] for change in data: result.append( ConfigurationChangeModel( type_of_change=change["type_of_change"], index=change["index"], target_name=change["target_name"], origin_name=change["origin_name"])) return result def _createMaterialOutputModel( self, material_data: Dict[str, Any]) -> "MaterialOutputModel": material_manager = CuraApplication.getInstance().getMaterialManager() material_group_list = material_manager.getMaterialGroupListByGUID( material_data["guid"]) # Sort the material groups by "is_read_only = True" first, and then the name alphabetically. read_only_material_group_list = list( filter(lambda x: x.is_read_only, material_group_list)) non_read_only_material_group_list = list( filter(lambda x: not x.is_read_only, material_group_list)) material_group = None if read_only_material_group_list: read_only_material_group_list = sorted( read_only_material_group_list, key=lambda x: x.name) material_group = read_only_material_group_list[0] elif non_read_only_material_group_list: non_read_only_material_group_list = sorted( non_read_only_material_group_list, key=lambda x: x.name) material_group = non_read_only_material_group_list[0] if material_group: container = material_group.root_material_node.getContainer() color = container.getMetaDataEntry("color_code") brand = container.getMetaDataEntry("brand") material_type = container.getMetaDataEntry("material") name = container.getName() else: Logger.log( "w", "Unable to find material with guid {guid}. Using data as provided by cluster" .format(guid=material_data["guid"])) color = material_data["color"] brand = material_data["brand"] material_type = material_data["material"] name = "Empty" if material_data[ "material"] == "empty" else "Unknown" return MaterialOutputModel(guid=material_data["guid"], type=material_type, brand=brand, color=color, name=name) def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None: # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer. # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping. self._printer_uuid_to_unique_name_mapping[ data["uuid"]] = data["unique_name"] definitions = ContainerRegistry.getInstance().findDefinitionContainers( name=data["machine_variant"]) if not definitions: Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"]) return machine_definition = definitions[0] printer.updateName(data["friendly_name"]) printer.updateKey(data["uuid"]) printer.updateType(data["machine_variant"]) # Do not store the build plate information that comes from connect if the current printer has not build plate information if "build_plate" in data and machine_definition.getMetaDataEntry( "has_variant_buildplates", False): printer.updateBuildplateName(data["build_plate"]["type"]) if not data["enabled"]: printer.updateState("disabled") else: printer.updateState(data["status"]) for index in range(0, self._number_of_extruders): extruder = printer.extruders[index] try: extruder_data = data["configuration"][index] except IndexError: break extruder.updateHotendID(extruder_data.get("print_core_id", "")) material_data = extruder_data["material"] if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data[ "guid"]: material = self._createMaterialOutputModel(material_data) extruder.updateActiveMaterial(material) def _removeJob(self, job: UM3PrintJobOutputModel) -> bool: if job not in self._print_jobs: return False if job.assignedPrinter: job.assignedPrinter.updateActivePrintJob(None) job.stateChanged.disconnect(self._printJobStateChanged) self._print_jobs.remove(job) return True def _removePrinter(self, printer: PrinterOutputModel) -> None: self._printers.remove(printer) if self._active_printer == printer: self._active_printer = None self.activePrinterChanged.emit() ## Sync the material profiles in Cura with the printer. # # This gets called when connecting to a printer as well as when sending a # print. def sendMaterialProfiles(self) -> None: job = SendMaterialJob(device=self) job.run()
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))