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))
Exemple #3
0
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()
Exemple #5
0
class SculptoPrintOutputDevice(PrinterOutputDevice):
    def __init__(self, key, address, port, properties):
        super().__init__(key)

        self._address = address
        self._port = port
        self._path = properties["path"] if "path" in properties else "/"
        self._key = key
        self._properties = properties  # Properties dict as provided by zero conf

        self._gcode = None
        self._auto_print = True

        ##  We start with a single extruder, but update this when we get data from Sculptoprint
        self._num_extruders = 1

        self._api_version = "1"
        self._api_prefix = "api/"
        self._api_header = "X-Api-Key"
        self._api_key = None

        self._base_url = "http://%s:%d/" % (self._address, self._port)
        self._api_url = self._base_url + self._api_prefix

        self.setPriority(
            2
        )  # Make sure the output device gets selected above local file output
        self.setName(key)
        self.setShortDescription(
            i18n_catalog.i18nc("@action:button", "Print on Sculpto"))
        self.setDescription(
            i18n_catalog.i18nc("@properties:tooltip", "Print on Sculpto"))
        self.setIconName("print")
        self.setConnectionText(
            i18n_catalog.i18nc("@info:status",
                               "Connected to Sculpto on {0}").format(
                                   self._key))

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

        ##  Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to)
        self._printer_request = None
        self._printer_reply = None

        self._print_job_request = None
        self._print_job_reply = None

        self._image_request = None
        self._image_reply = None

        self._post_request = None
        self._post_reply = None
        self._post_multi_part = None
        self._post_part = None

        self._job_request = None
        self._job_reply = None

        self._command_request = None
        self._command_reply = None

        self._progress_message = None
        self._error_message = None
        self._connection_message = None

        self._update_timer = QTimer()
        self._update_timer.setInterval(
            2000)  # TODO; Add preference for update interval
        self._update_timer.setSingleShot(False)
        self._update_timer.timeout.connect(self._update)

        self._connection_state_before_timeout = None

        self._is_printing = False
        self.estimated_total = 0
        self.starting_time = 0

        self._last_response_time = None
        self._last_request_time = None
        self._response_timeout_time = 5
        self._recreate_network_manager_time = 30  # If we have no connection, re-create network manager every 30 sec.
        self._recreate_network_manager_count = 1

    def getProperties(self):
        return self._properties

    @pyqtSlot(str, result=str)
    def getProperty(self, key):
        key = key.encode("utf-8")
        if key in self._properties:
            return self._properties.get(key, b"").decode("utf-8")
        else:
            return ""

    ##  Get the unique key of this machine
    #   \return key String containing the key of the machine.
    @pyqtSlot(result=str)
    def getKey(self):
        return self._key

    ##  Set the API key of this SculptoPrint instance
    def setApiKey(self, api_key):
        self._api_key = api_key

    ##  Name of the instance (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def name(self):
        return self._key

    ##  Version (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def SculptoprintVersion(self):
        return self._properties.get(b"version", b"").decode("utf-8")

    ## IPadress of this instance
    @pyqtProperty(str, constant=True)
    def ipAddress(self):
        return self._address

    ## port of this instance
    @pyqtProperty(int, constant=True)
    def port(self):
        return self._port

    ## path of this instance
    @pyqtProperty(str, constant=True)
    def path(self):
        return self._path

    ## absolute url of this instance
    @pyqtProperty(str, constant=True)
    def baseURL(self):
        return self._base_url

    def _update(self):
        if self._last_response_time:
            time_since_last_response = time() - self._last_response_time
        else:
            time_since_last_response = 0
        if self._last_request_time:
            time_since_last_request = time() - self._last_request_time
        else:
            time_since_last_request = float(
                "inf")  # An irrelevantly large number of seconds

        # Connection is in timeout, check if we need to re-start the connection.
        # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows.
        # Re-creating the QNetworkManager seems to fix this issue.
        if self._last_response_time and self._connection_state_before_timeout:
            if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count:
                self._recreate_network_manager_count += 1
                # It can happen that we had a very long timeout (multiple times the recreate time).
                # In that case we should jump through the point that the next update won't be right away.
                while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time:
                    self._recreate_network_manager_count += 1
                Logger.log(
                    "d",
                    "Timeout lasted over 30 seconds (%.1fs), re-checking connection.",
                    time_since_last_response)
                self._createNetworkManager()
                return

        # Check if we have an connection in the first place.
        if not self._manager.networkAccessible():
            if not self._connection_state_before_timeout:
                Logger.log(
                    "d",
                    "The network connection seems to be disabled. Going into timeout mode"
                )
                self._connection_state_before_timeout = self._connection_state
                self.setConnectionState(ConnectionState.error)
                self._connection_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "The connection with the network was lost."))
                self._connection_message.show()
                # Check if we were uploading something. Abort if this is the case.
                # Some operating systems handle this themselves, others give weird issues.
                try:
                    if self._post_reply:
                        Logger.log(
                            "d",
                            "Stopping post upload because the connection was lost."
                        )
                        try:
                            self._post_reply.uploadProgress.disconnect(
                                self._onUploadProgress)
                        except TypeError:
                            pass  # The disconnection can fail on mac in some cases. Ignore that.

                        self._post_reply.abort()
                        self._progress_message.hide()
                except RuntimeError:
                    self._post_reply = None  # It can happen that the wrapped c++ object is already deleted.
            return
        else:
            if not self._connection_state_before_timeout:
                self._recreate_network_manager_count = 1

        # Check that we aren't in a timeout state
        if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout:
            if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time:
                # Go into timeout state.
                Logger.log(
                    "d",
                    "We did not receive a response for %s seconds, so it seems SculptoPrint is no longer accesible.",
                    time() - self._last_response_time)
                self._connection_state_before_timeout = self._connection_state
                self._connection_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "The connection with Sculpto was lost. Check your network-connections."
                    ))
                self._connection_message.show()
                self.setConnectionState(ConnectionState.error)

        ## Request 'general' printer data
        url = QUrl(self._base_url + "temperature")
        self._printer_request = QNetworkRequest(url)
        self._printer_request.setRawHeader(self._api_header.encode(),
                                           self._api_key.encode())
        self._printer_reply = self._manager.get(self._printer_request)

        ## Request print_job data
        url = QUrl(self._base_url + "progress")
        self._job_request = QNetworkRequest(url)
        self._job_request.setRawHeader(self._api_header.encode(),
                                       self._api_key.encode())
        self._job_reply = self._manager.get(self._job_request)

    def _createNetworkManager(self):
        if self._manager:
            self._manager.finished.disconnect(self._onRequestFinished)

        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

    def close(self):
        self._updateJobState("")
        self.setConnectionState(ConnectionState.closed)
        if self._progress_message:
            self._progress_message.hide()
        if self._error_message:
            self._error_message.hide()
        self._update_timer.stop()
        self.newImage.emit()

    def requestWrite(self, node, file_name=None, filter_by_machine=False):
        self.writeStarted.emit(self)
        self._gcode = getattr(
            Application.getInstance().getController().getScene(), "gcode_list")

        self.startPrint()

    def isConnected(self):
        return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error

    ##  Start requesting data from the instance
    def connect(self):
        #self.close()  # Ensure that previous connection (if any) is killed.
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if not global_container_stack:
            return

        self._createNetworkManager()

        self.setConnectionState(ConnectionState.connecting)
        self._update(
        )  # Manually trigger the first update, as we don't want to wait a few secs before it starts.

        Logger.log("d", "Connection with instance %s with ip %s started",
                   self._key, self._address)
        self._update_timer.start()

        self._last_response_time = None
        self.setAcceptsCommands(False)
        self.setConnectionText(
            i18n_catalog.i18nc("@info:status",
                               "Connecting to Sculpto on {0}").format(
                                   self._key))

    ##  Stop requesting data from the instance
    def disconnect(self):
        Logger.log("d", "Connection with instance %s with ip %s stopped",
                   self._key, self._address)
        self.close()

    newImage = pyqtSignal()

    def _setJobState(self, job_state):
        if job_state == "abort":
            Logger.log("d", "Should abort!")
            return
            #command = "cancel"
        elif job_state == "print":
            if self.jobState == "paused":
                command = "pause"
            else:
                command = "start"
        elif job_state == "pause":
            command = "pause"

        if command:
            self._sendCommand(command)

    def stopPrint(self):
        self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)

        ##  Create parts (to be placed inside multipart)
        self._post_part = QHttpPart()
        self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader,
                                  "form-data; name=\"cool\"")
        self._post_part.setBody(b"true")
        self._post_multi_part.append(self._post_part)

        url = QUrl(self._base_url + "stop_print")

        ##  Create the QT request
        self._post_request = QNetworkRequest(url)
        self._post_request.setRawHeader(self._api_header.encode(),
                                        self._api_key.encode())

        ##  Post request + data
        self._post_reply = self._manager.post(self._post_request,
                                              self._post_multi_part)

    def startPrint(self):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if not global_container_stack:
            return

        self._auto_print = parseBool(
            global_container_stack.getMetaDataEntry("sculptoprint_auto_print",
                                                    True))
        if self._auto_print:
            Application.getInstance().showPrintMonitor.emit(True)

        if self.jobState != "ready" and self.jobState != "":
            self._error_message = Message(
                i18n_catalog.i18nc(
                    "@info:status",
                    "Sculpto is printing. Unable to start a new job."))
            self._error_message.show()
            return
        try:
            self._progress_message = Message(
                i18n_catalog.i18nc("@info:status", "Sending data to Sculpto"),
                0, False, -1)
            self._progress_message.show()

            ## Mash the data into single string
            single_string_file_data = ""
            last_process_events = time()
            for line in self._gcode:
                single_string_file_data += line
                if time() > last_process_events + 0.05:
                    # Ensure that the GUI keeps updated at least 20 times per second.
                    QCoreApplication.processEvents()
                    last_process_events = time()

            file_name = "%s.gcode" % Application.getInstance(
            ).getPrintInformation().jobName

            ##  Create multi_part request
            self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)

            ##  Create parts (to be placed inside multipart)
            self._post_part = QHttpPart()
            self._post_part.setHeader(
                QNetworkRequest.ContentDispositionHeader,
                "form-data; name=\"file\"; filename=\"%s\"" % file_name)
            self._post_part.setBody(single_string_file_data.encode())
            self._post_multi_part.append(self._post_part)

            url = QUrl(self._base_url + "upload_and_print")

            self.setJobName(file_name)

            ##  Create the QT request
            self._post_request = QNetworkRequest(url)
            self._post_request.setRawHeader(self._api_header.encode(),
                                            self._api_key.encode())

            ##  Post request + data
            self._post_reply = self._manager.post(self._post_request,
                                                  self._post_multi_part)
            self._post_reply.uploadProgress.connect(self._onUploadProgress)

            self._gcode = None

        except IOError:
            self._progress_message.hide()
            self._error_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Unable to send data to SculptoPrint."))
            self._error_message.show()
        except Exception as e:
            self._progress_message.hide()
            Logger.log(
                "e",
                "An exception occurred in network connection: %s" % str(e))

    def _sendCommand(self, command):
        url = QUrl(self._api_url + "job")
        self._command_request = QNetworkRequest(url)
        self._command_request.setRawHeader(self._api_header.encode(),
                                           self._api_key.encode())
        self._command_request.setHeader(QNetworkRequest.ContentTypeHeader,
                                        "application/json")

        data = "{\"command\": \"%s\"}" % command
        self._command_reply = self._manager.post(self._command_request,
                                                 data.encode())
        Logger.log("d", "Sent command to SculptoPrint instance: %s", data)

    def _setTargetBedTemperature(self, temperature):
        Logger.log("d", "Setting bed temperature to %s", temperature)
        self._sendCommand("M140 S%s" % temperature)

    def _setTargetHotendTemperature(self, index, temperature):
        Logger.log("d", "Setting hotend %s temperature to %s", index,
                   temperature)
        self._sendCommand("M104 T%s S%s" % (index, temperature))

    def _setHeadPosition(self, x, y, z, speed):
        self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))

    def _setHeadX(self, x, speed):
        self._sendCommand("G0 X%s F%s" % (x, speed))

    def _setHeadY(self, y, speed):
        self._sendCommand("G0 Y%s F%s" % (y, speed))

    def _setHeadZ(self, z, speed):
        self._sendCommand("G0 Y%s F%s" % (z, speed))

    def _homeHead(self):
        self._sendCommand("G28")

    def _homeBed(self):
        self._sendCommand("G28 Z")

    def _moveHead(self, x, y, z, speed):
        self._sendCommand("G91")
        self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
        self._sendCommand("G90")

    ##  Handler for all requests that have finished.
    def _onRequestFinished(self, reply):
        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Received a timeout on a request to the instance")
            self._connection_state_before_timeout = self._connection_state
            self.setConnectionState(ConnectionState.error)
            return

        if self._connection_state_before_timeout and reply.error(
        ) == QNetworkReply.NoError:  #  There was a timeout, but we got a correct answer again.
            if self._last_response_time:
                Logger.log(
                    "d",
                    "We got a response from the instance after %s of silence",
                    time() - self._last_response_time)
            self.setConnectionState(self._connection_state_before_timeout)
            self._connection_state_before_timeout = None

        if reply.error() == QNetworkReply.NoError:
            self._last_response_time = time()

        http_status_code = reply.attribute(
            QNetworkRequest.HttpStatusCodeAttribute)
        if not http_status_code:
            # Received no or empty reply
            return
        Logger.log("w", "Got response")
        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "temperature" in reply.url().toString(
            ):  # Status update from /temperature.
                if http_status_code == 200:
                    if not self.acceptsCommands:
                        self.setAcceptsCommands(True)
                        self.setConnectionText(
                            i18n_catalog.i18nc(
                                "@info:status",
                                "Connected to SculptoPrint on {0}").format(
                                    self._key))

                    if self._connection_state == ConnectionState.connecting:
                        self.setConnectionState(ConnectionState.connected)
                    json_data = json.loads(
                        bytes(reply.readAll()).decode("utf-8"))

                    self._setHotendTemperature(0, json_data['payload'])
                    '''
                    job_state = "offline"
                    if json_data["state"]["flags"]["error"]:
                        job_state = "error"
                    elif json_data["state"]["flags"]["paused"]:
                        job_state = "paused"
                    elif json_data["state"]["flags"]["printing"]:
                        job_state = "printing"
                    elif json_data["state"]["flags"]["ready"]:
                        job_state = "ready"
                    self._updateJobState(job_state)
                    '''
                elif http_status_code == 401:
                    self.setAcceptsCommands(False)
                    self.setConnectionText(
                        i18n_catalog.i18nc(
                            "@info:status",
                            "Sculpto on {0} does not allow access to print").
                        format(self._key))
                else:
                    pass  # TODO: Handle errors

            elif "progress" in reply.url().toString(
            ):  # Status update from /progress:
                if http_status_code == 500:
                    self.setTimeElapsed(0)
                    self.setTimeTotal(0)
                    self._updateJobState("ready")
                    self.setProgress(0)
                    self.setJobName("Waiting for Print")
                    self._is_printing = False
                if http_status_code == 200:
                    json_data = json.loads(
                        bytes(reply.readAll()).decode("utf-8"))
                    Logger.log("d",
                               "Progress: {0}".format(json_data["payload"]))
                    self.setProgress(json_data["payload"])
                    self._updateJobState("printing")
                    self._is_printing = True
                    self.setTimeElapsed(time() - self.starting_time)
                    '''
                    progress = json_data["progress"]["completion"]
                    if progress:
                        self.setProgress(progress)

                    if json_data["progress"]["printTime"]:
                        self.setTimeElapsed(json_data["progress"]["printTime"])
                        if json_data["progress"]["printTimeLeft"]:
                            self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"])
                        elif json_data["job"]["estimatedPrintTime"]:
                            self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"]))
                        elif progress > 0:
                            self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100))
                        else:
                            self.setTimeTotal(0)
                    else:
                        self.setTimeElapsed(0)
                        self.setTimeTotal(0)
                    self.setJobName(json_data["job"]["file"]["name"])
                    '''
                else:
                    pass  # TODO: Handle errors

        elif reply.operation() == QNetworkAccessManager.PostOperation:
            if "upload_and_print" in reply.url().toString(
            ):  # Result from /upload_and_print command:
                if http_status_code == 201:
                    Logger.log("d", "Successfully uploaded and printing!")
                    print_information = Application.getInstance(
                    ).getPrintInformation().currentPrintTime
                    self.estimated_total = int(
                        print_information.getDisplayString(0))
                    self.starting_time = time()
                    self.setTimeTotal(self.estimated_total)
                    self.setTimeElapsed(0)
                else:
                    pass  # TODO: Handle errors

                reply.uploadProgress.disconnect(self._onUploadProgress)
                self._progress_message.hide()
                global_container_stack = Application.getInstance(
                ).getGlobalContainerStack()
                if not self._auto_print:
                    location = reply.header(QNetworkRequest.LocationHeader)
                    if location:
                        file_name = QUrl(
                            reply.header(QNetworkRequest.LocationHeader).
                            toString()).fileName()
                        message = Message(
                            i18n_catalog.i18nc(
                                "@info:status",
                                "Saved to SculptoPrint as {0}").format(
                                    file_name))
                    else:
                        message = Message(
                            i18n_catalog.i18nc("@info:status",
                                               "Saved to SculptoPrint"))
                    message.addAction(
                        "open_browser",
                        i18n_catalog.i18nc("@action:button",
                                           "Open SculptoPrint..."), "globe",
                        i18n_catalog.i18nc(
                            "@info:tooltip",
                            "Open the SculptoPrint web interface"))
                    message.actionTriggered.connect(
                        self._onMessageActionTriggered)
                    message.show()

            elif "stop_print" in reply.url().toString(
            ):  # Result from /stop_print command:
                if http_status_code == 200:
                    Logger.log("d", "Printing stopped")
                    self._updateJobState("ready")
                    self.setProgress(0)
                    self._is_printing = False
                else:
                    pass  # TODO: Handle errors

        else:
            Logger.log(
                "d", "SculptoPrintOutputDevice got an unhandled operation %s",
                reply.operation())

    def _onUploadProgress(self, bytes_sent, bytes_total):
        if bytes_total > 0:
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()

            progress = bytes_sent / bytes_total * 100
            if progress < 100:
                if progress > self._progress_message.getProgress():
                    self._progress_message.setProgress(progress)
            else:
                self._progress_message.hide()
                self._progress_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "Storing gcode on Sculpto and starting print"), 0,
                    False, -1)
                self._progress_message.show()
        else:
            self._progress_message.setProgress(0)

    def _onMessageActionTriggered(self, message, action):
        if action == "open_browser":
            QDesktopServices.openUrl(QUrl(self._base_url))
Exemple #6
0
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()
Exemple #7
0
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
Exemple #8
0
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))
Exemple #9
0
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()
Exemple #10
0
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)
Exemple #13
0
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()
Exemple #16
0
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))
Exemple #17
0
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()
Exemple #18
0
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()
Exemple #19
0
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)
Exemple #21
0
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
Exemple #23
0
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()
Exemple #24
0
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))