def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
                     file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:

        # Show an error message if we're already sending a job.
        if self._progress.visible:
            message = Message(
                text = I18N_CATALOG.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job."),
                title = I18N_CATALOG.i18nc("@info:title", "Cloud error"),
                lifetime = 10
            )
            message.show()
            return

        if self._uploaded_print_job:
            # The mesh didn't change, let's not upload it again
            self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted)
            return

        # Indicate we have started sending a job.
        self.writeStarted.emit(self)

        mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion)
        if not mesh_format.is_valid:
            Logger.log("e", "Missing file or mesh writer!")
            return self._onUploadError(I18N_CATALOG.i18nc("@info:status", "Could not export print job."))

        mesh = mesh_format.getBytes(nodes)

        self._tool_path = mesh
        request = CloudPrintJobUploadRequest(
            job_name = file_name or mesh_format.file_extension,
            file_size = len(mesh),
            content_type = mesh_format.mime_type,
        )
        self._api.requestUpload(request, self._onPrintJobCreated)
Exemple #2
0
class BackendProxy(QObject):
    def __init__(self, parent = None):
        super().__init__(parent)
        self._backend = Application.getInstance().getBackend()
        self._progress = -1;
        self._messageDisplayed = False
        self._message = None
        if self._backend:
            self._backend.processingProgress.connect(self._onProcessingProgress)

    processingProgress = pyqtSignal(float, arguments = ["amount"])
    
    @pyqtProperty(float, notify = processingProgress)
    def progress(self):
        if self._progress > 0 and self._progress < 1 and self._messageDisplayed == False:
            self._message = Message(i18n_catalog.i18n("Slicing in Process: "), 0, False, self._progress)
            self._message.show()
            self._messageDisplayed = True
        if self._progress >= 1 and self._messageDisplayed == True:
            self._message.hide()
            self._messageDisplayed = False
        return self._progress

    def _onProcessingProgress(self, amount):
        self._progress = amount
        self.processingProgress.emit(amount)
Exemple #3
0
    def run(self):
        loading_message = Message(i18n_catalog.i18nc("Loading mesh message, {0} is file name", "Loading {0}").format(self._filename), lifetime = 0, dismissable = False)
        loading_message.setProgress(-1)
        loading_message.show()

        mesh = self._handler.read(self._filename, self._device)

        # Scale down to maximum bounds size if that is available
        if hasattr(Application.getInstance().getController().getScene(), "_maximum_bounds"):
            max_bounds = Application.getInstance().getController().getScene()._maximum_bounds
            bbox = mesh.getExtents()

            if max_bounds.width < bbox.width or max_bounds.height < bbox.height or max_bounds.depth < bbox.depth:
                largest_dimension = max(bbox.width, bbox.height, bbox.depth)

                scale_factor = 1.0
                if largest_dimension == bbox.width:
                    scale_factor = max_bounds.width / bbox.width
                elif largest_dimension == bbox.height:
                    scale_factor = max_bounds.height / bbox.height
                else:
                    scale_factor = max_bounds.depth / bbox.depth

                matrix = Matrix()
                matrix.setByScaleFactor(scale_factor)
                mesh = mesh.getTransformed(matrix)

        self.setResult(mesh)

        loading_message.hide()
        result_message = Message(i18n_catalog.i18nc("Finished loading mesh message, {0} is file name", "Loaded {0}").format(self._filename))
        result_message.show()
 def _onApiError(self, errors: List[CloudError] = None) -> None:
     Logger.log("w", str(errors))
     message = Message(
         text = self.I18N_CATALOG.i18nc("@info:description", "There was an error connecting to the cloud."),
         title = self.I18N_CATALOG.i18nc("@info:title", "Error"),
         lifetime = 10
     )
     message.show()
Exemple #5
0
 def ejectDevice(self, device):
     result = self.performEjectDevice(device)
     if result:
         message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(device.getName()))
         message.show()
     else:
         message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Maybe it is still in use?").format(device.getName()))
         message.show()
Exemple #6
0
    def run(self):
        status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"), lifetime = 0, dismissable=False, progress = 0)
        status_message.show()
        arranger = Arrange.create(fixed_nodes = self._fixed_nodes)

        # Collect nodes to be placed
        nodes_arr = []  # fill with (size, node, offset_shape_arr, hull_shape_arr)
        for node in self._nodes:
            offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset)
            nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr))

        # Sort the nodes with the biggest area first.
        nodes_arr.sort(key=lambda item: item[0])
        nodes_arr.reverse()

        # Place nodes one at a time
        start_priority = 0
        last_priority = start_priority
        last_size = None
        grouped_operation = GroupedOperation()
        found_solution_for_all = True
        for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
            # For performance reasons, we assume that when a location does not fit,
            # it will also not fit for the next object (while what can be untrue).
            # We also skip possibilities by slicing through the possibilities (step = 10)
            if last_size == size:  # This optimization works if many of the objects have the same size
                start_priority = last_priority
            else:
                start_priority = 0
            best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
            x, y = best_spot.x, best_spot.y
            node.removeDecorator(ZOffsetDecorator)
            if node.getBoundingBox():
                center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
            else:
                center_y = 0
            if x is not None:  # We could find a place
                last_size = size
                last_priority = best_spot.priority

                arranger.place(x, y, hull_shape_arr)  # take place before the next one

                grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
            else:
                Logger.log("d", "Arrange all: could not find spot!")
                found_solution_for_all = False
                grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, - idx * 20), set_position = True))

            status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
            Job.yieldThread()

        grouped_operation.push()

        status_message.hide()

        if not found_solution_for_all:
            no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"))
            no_full_solution_message.show()
Exemple #7
0
class ReadFileJob(Job):
    def __init__(self, filename: str, handler: Optional[FileHandler] = None) -> None:
        super().__init__()
        self._filename = filename
        self._handler = handler
        self._loading_message = None  # type: Optional[Message]

    def getFileName(self):
        return self._filename

    def run(self) -> None:
        if self._handler is None:
            Logger.log("e", "FileHandler was not set.")
            return None
        reader = self._handler.getReaderForFile(self._filename)
        if not reader:
            result_message = Message(i18n_catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Cannot open files of the type of <filename>{0}</filename>", self._filename), lifetime=0, title = i18n_catalog.i18nc("@info:title", "Invalid File"))
            result_message.show()
            return

        # Give the plugin a chance to display a dialog before showing the loading UI
        try:
            pre_read_result = reader.preRead(self._filename)
        except:
            Logger.logException("e", "Failed to pre-read the file %s", self._filename)
            pre_read_result = MeshReader.PreReadResult.failed

        if pre_read_result != MeshReader.PreReadResult.accepted:
            if pre_read_result == MeshReader.PreReadResult.failed:
                result_message = Message(i18n_catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to load <filename>{0}</filename>", self._filename),
                                         lifetime=0,
                                         title = i18n_catalog.i18nc("@info:title", "Invalid File"))
                result_message.show()
            return

        self._loading_message = Message(self._filename,
                                        lifetime=0,
                                        dismissable=False,
                                        title = i18n_catalog.i18nc("@info:title", "Loading"))
        self._loading_message.setProgress(-1)
        self._loading_message.show()

        Job.yieldThread()  # Yield to any other thread that might want to do something else.
        begin_time = time.time()
        try:
            self.setResult(self._handler.readerRead(reader, self._filename))
        except:
            Logger.logException("e", "Exception occurred while loading file %s", self._filename)
        finally:
            end_time = time.time()
            Logger.log("d", "Loading file took %0.1f seconds", end_time - begin_time)
            if self._result is None:
                self._loading_message.hide()
                result_message = Message(i18n_catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to load <filename>{0}</filename>", self._filename), lifetime=0, title = i18n_catalog.i18nc("@info:title", "Invalid File"))
                result_message.show()
                return
            self._loading_message.hide()
    def _onActionTriggered(self, message, action):
        if action == "eject":
            if Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("RemovableDriveOutputDevice").ejectDevice(self):
                message.hide()

                eject_message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(self.getName()), title = catalog.i18nc("@info:title", "Safely Remove Hardware"))
            else:
                eject_message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Another program may be using the drive.").format(self.getName()), title = catalog.i18nc("@info:title", "Warning"))
            eject_message.show()
 def _showRequestFailedMessage(self, reply):
     if reply is not None:
         Logger.log("w", "Unable to send print job to group {cluster_name}: {error_string} ({error})".format(
             cluster_name = self.getName(),
             error_string = str(reply.errorString()),
             error = str(reply.error())))
         error_message_template = i18n_catalog.i18nc("@info:status", "Unable to send print job to group {cluster_name}.")
         message = Message(text=error_message_template.format(
             cluster_name = self.getName()))
         message.show()
Exemple #10
0
    def requestWrite(self, node, file_name=None, filter_by_machine=False):
        if self._writing:
            raise OutputDeviceError.DeviceBusyError()

        file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
        machine_file_formats = Application.getInstance().getMachineManager().getActiveMachineInstance().getMachineDefinition().getFileFormats()
        file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats))
        if len(file_formats) == 0:
            Logger.log("e", "There are no file formats available to write with!")
            raise OutputDeviceError.WriteRequestFailedError()
        writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"])
        extension = file_formats[0]["extension"]

        if file_name == None:
            for n in BreadthFirstIterator(node):
                if n.getMeshData():
                    file_name = n.getName()
                    if file_name:
                        break

        if not file_name:
            Logger.log("e", "Could not determine a proper file name when trying to print, aborting")
            raise OutputDeviceError.WriteRequestFailedError()

        temp_dir = os.path.join(tempfile.gettempdir(), "Kiddo")
        if not os.path.exists(temp_dir):
            os.mkdir(temp_dir)

        if extension:
            extension = "." + extension
        file_name = os.path.join(temp_dir, os.path.splitext(file_name)[0] + extension)
        
        try:
            Logger.log("d", "Writing to %s", file_name)
            stream = open(file_name, "wt")
            job = WriteMeshJob(writer, stream, node, MeshWriter.OutputMode.TextMode)
            job.setFileName(file_name)
            job.progress.connect(self._onProgress)
            job.finished.connect(self._onFinished)

            message = Message(catalog.i18nc("@info:progress", "Preparing print job"), 0, False, -1)
            message.show()

            self.writeStarted.emit(self)

            job._message = message
            self._writing = True
            job.start()
        except PermissionError as e:
            Logger.log("e", "Permission denied when trying to write to %s: %s", file_name, str(e))
            raise OutputDeviceError.PermissionDeniedError(e)
        except OSError as e:
            Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e))
            raise OutputDeviceError.WriteRequestFailedError(e)
    def requestWrite(self, nodes, file_name = None, filter_by_machine = False):
        filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do)
        if self._writing:
            raise OutputDeviceError.DeviceBusyError()

        # Formats supported by this application (File types that we can actually write)
        file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
        if filter_by_machine:
            container = Application.getInstance().getGlobalContainerStack().findContainer({"file_formats": "*"})

            # Create a list from supported file formats string
            machine_file_formats = [file_type.strip() for file_type in container.getMetaDataEntry("file_formats").split(";")]

            # Take the intersection between file_formats and machine_file_formats.
            file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats))

        if len(file_formats) == 0:
            Logger.log("e", "There are no file formats available to write with!")
            raise OutputDeviceError.WriteRequestFailedError()

        # Just take the first file format available.
        writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"])
        extension = file_formats[0]["extension"]

        if file_name is None:
            file_name = self._automaticFileName(nodes)

        if extension:  # Not empty string.
            extension = "." + extension
        file_name = os.path.join(self.getId(), os.path.splitext(file_name)[0] + extension)

        try:
            Logger.log("d", "Writing to %s", file_name)
            # Using buffering greatly reduces the write time for many lines of gcode
            self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8")
            job = WriteMeshJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode)
            job.setFileName(file_name)
            job.progress.connect(self._onProgress)
            job.finished.connect(self._onFinished)

            message = Message(catalog.i18nc("@info:progress", "Saving to Removable Drive <filename>{0}</filename>").format(self.getName()), 0, False, -1)
            message.show()

            self.writeStarted.emit(self)

            job._message = message
            self._writing = True
            job.start()
        except PermissionError as e:
            Logger.log("e", "Permission denied when trying to write to %s: %s", file_name, str(e))
            raise OutputDeviceError.PermissionDeniedError(catalog.i18nc("@info:status", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e
        except OSError as e:
            Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e))
            raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e
    def run(self):
        if not self._url:
            Logger.log("e", "Can not check for a new release. URL not set!")
            return

        try:
            application_name = Application.getInstance().getApplicationName()
            headers = {"User-Agent": "%s - %s" % (application_name, Application.getInstance().getVersion())}
            request = urllib.request.Request(self._url, headers = headers)
            current_version_file = urllib.request.urlopen(request)
            reader = codecs.getreader("utf-8")

            # get machine name from the definition container
            machine_name = self._container.definition.getName()
            machine_name_parts = machine_name.lower().split(" ")

            # If it is not None, then we compare between the checked_version and the current_version
            # Now we just do that if the active printer is Ultimaker 3 or Ultimaker 3 Extended or any
            # other Ultimaker 3 that will come in the future
            if len(machine_name_parts) >= 2 and machine_name_parts[:2] == ["ultimaker", "3"]:
                Logger.log("i", "You have a UM3 in printer list. Let's check the firmware!")

                # Nothing to parse, just get the string
                # TODO: In the future may be done by parsing a JSON file with diferent version for each printer model
                current_version = reader(current_version_file).readline().rstrip()

                # If it is the first time the version is checked, the checked_version is ''
                checked_version = Preferences.getInstance().getValue("info/latest_checked_firmware")

                # If the checked_version is '', it's because is the first time we check firmware and in this case
                # we will not show the notification, but we will store it for the next time
                Preferences.getInstance().setValue("info/latest_checked_firmware", current_version)
                Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s", machine_name, checked_version, current_version)

                # The first time we want to store the current version, the notification will not be shown,
                # because the new version of Cura will be release before the firmware and we don't want to
                # notify the user when no new firmware version is available.
                if (checked_version != "") and (checked_version != current_version):
                    Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE")
                    message = Message(i18n_catalog.i18nc("@info Don't translate {machine_name}, since it gets replaced by a printer name!", "New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format(machine_name = machine_name),
                                      title = i18n_catalog.i18nc("@info:title The %s gets replaced with the printer name.", "New %s firmware available") % machine_name)
                    message.addAction("download", i18n_catalog.i18nc("@action:button", "How to update"), "[no_icon]", "[no_description]")

                    # If we do this in a cool way, the download url should be available in the JSON file
                    if self._set_download_url_callback:
                        self._set_download_url_callback("https://ultimaker.com/en/resources/23129-updating-the-firmware?utm_source=cura&utm_medium=software&utm_campaign=hw-update")
                    message.actionTriggered.connect(self._callback)
                    message.show()

        except Exception as e:
            Logger.log("w", "Failed to check for new version: %s", e)
            if not self.silent:
                Message(i18n_catalog.i18nc("@info", "Could not access update information.")).show()
            return
    def checkNewVersion(self, silent = False):
        no_new_version = True

        application_name = Application.getInstance().getApplicationName()
        Logger.log("i", "Checking for new version of %s" % application_name)

        try:
            latest_version_file = urllib.request.urlopen("http://software.ultimaker.com/latest.json")
        except Exception as e:
            Logger.log("e", "Failed to check for new version. %s" %e)
            if not silent:
                Message(i18n_catalog.i18nc("@info", "Could not access update information.")).show()
            return

        try:
            reader = codecs.getreader("utf-8")
            data = json.load(reader(latest_version_file))
            try:
                if Application.getInstance().getVersion() is not "master":
                    local_version = Version(Application.getInstance().getVersion())
                else:
                    if not silent:
                        Message(i18n_catalog.i18nc("@info", "The version you are using does not support checking for updates.")).show()
                    return
            except ValueError:
                Logger.log("w", "Could not determine application version from string %s, not checking for updates", Application.getInstance().getVersion())
                if not silent:
                    Message(i18n_catalog.i18nc("@info", "The version you are using does not support checking for updates.")).show()
                return

            if application_name in data:
                for key, value in data[application_name].items():
                    if "major" in value and "minor" in value and "revision" in value and "url" in value:
                        os = key
                        if platform.system() == os: #TODO: add architecture check
                            newest_version = Version([int(value["major"]), int(value["minor"]), int(value["revision"])])
                            if local_version < newest_version:
                                Logger.log("i", "Found a new version of the software. Spawning message")
                                message = Message(i18n_catalog.i18nc("@info", "A new version is available!"))
                                message.addAction("download", i18n_catalog.i18nc("@action:button", "Download"), "[no_icon]", "[no_description]")
                                self._url = value["url"]
                                message.actionTriggered.connect(self.actionTriggered)
                                message.show()
                                no_new_version = False
                                break
                    else:
                        Logger.log("e", "Could not find version information or download url for update.")
            else:
                Logger.log("e", "Did not find any version information for %s." % application_name)
        except Exception as e:
            Logger.log("e", "Exception in update checker: %s" % (e))

        if no_new_version and not silent:
            Message(i18n_catalog.i18nc("@info", "No new version was found.")).show()
Exemple #14
0
 def _onWriteToSDFinished(self, job):
     message = Message(self._i18n_catalog.i18nc("Saved to SD message, {0} is sdcard, {1} is filename", "Saved to SD Card {0} as {1}").format(job._sdcard, job.getFileName()))
     message.addAction(
         "eject",
         self._i18n_catalog.i18nc("Message action", "Eject"),
         "eject",
         self._i18n_catalog.i18nc("Message action tooltip, {0} is sdcard", "Eject SD Card {0}").format(job._sdcard)
     )
     message._sdcard = job._sdcard
     message.actionTriggered.connect(self._onMessageActionTriggered)
     message.show()
    def _writeToDevice(self, node, device_id):
        device = self._device_manager.getOutputDevice(device_id)
        if not device:
            return

        try:
            device.requestWrite(node)
        except OutputDeviceError.UserCanceledError:
            pass
        except OutputDeviceError.WriteRequestFailedError as e:
            message = Message(str(e))
            message.show()
Exemple #16
0
    def run(self):
        status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
                                 dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Object"))
        status_message.show()
        scene = Application.getInstance().getController().getScene()

        total_progress = len(self._objects) * self._count
        current_progress = 0

        root = scene.getRoot()
        arranger = Arrange.create(scene_root=root)
        nodes = []
        for node in self._objects:
            # If object is part of a group, multiply group
            current_node = node
            while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
                current_node = current_node.getParent()

            node_too_big = False
            if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300:
                offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset)
            else:
                node_too_big = True

            found_solution_for_all = True
            for i in range(self._count):
                # We do place the nodes one by one, as we want to yield in between.
                if not node_too_big:
                    node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr)
                if node_too_big or not solution_found:
                    found_solution_for_all = False
                    new_location = node.getPosition()
                    new_location = new_location.set(z = 100 - i * 20)
                    node.setPosition(new_location)

                nodes.append(node)
                current_progress += 1
                status_message.setProgress((current_progress / total_progress) * 100)
                Job.yieldThread()

            Job.yieldThread()

        if nodes:
            op = GroupedOperation()
            for new_node in nodes:
                op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
            op.push()
        status_message.hide()

        if not found_solution_for_all:
            no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Placing Object"))
            no_full_solution_message.show()
    def requestWrite(self, node, file_name = None, filter_by_machine = False):
        filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do)
        if self._writing:
            raise OutputDeviceError.DeviceBusyError()

        file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() #Formats supported by this application.
        if filter_by_machine:
            machine_file_formats = Application.getInstance().getMachineManager().getActiveMachineInstance().getMachineDefinition().getFileFormats()
            file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats)) #Take the intersection between file_formats and machine_file_formats.
        if len(file_formats) == 0:
            Logger.log("e", "There are no file formats available to write with!")
            raise OutputDeviceError.WriteRequestFailedError()
        writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"]) #Just take the first file format available.
        extension = file_formats[0]["extension"]

        if file_name == None:
            for n in BreadthFirstIterator(node):
                if n.getMeshData():
                    file_name = n.getName()
                    if file_name:
                        break

        if not file_name:
            Logger.log("e", "Could not determine a proper file name when trying to write to %s, aborting", self.getName())
            raise OutputDeviceError.WriteRequestFailedError()

        if extension: #Not empty string.
            extension = "." + extension
        file_name = os.path.join(self.getId(), os.path.splitext(file_name)[0] + extension)

        try:
            Logger.log("d", "Writing to %s", file_name)
            stream = open(file_name, "wt")
            job = WriteMeshJob(writer, stream, node, MeshWriter.OutputMode.TextMode)
            job.setFileName(file_name)
            job.progress.connect(self._onProgress)
            job.finished.connect(self._onFinished)

            message = Message(catalog.i18nc("@info:progress", "Saving to Removable Drive <filename>{0}</filename>").format(self.getName()), 0, False, -1)
            message.show()

            self.writeStarted.emit(self)

            job._message = message
            self._writing = True
            job.start()
        except PermissionError as e:
            Logger.log("e", "Permission denied when trying to write to %s: %s", file_name, str(e))
            raise OutputDeviceError.PermissionDeniedError() from e
        except OSError as e:
            Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e))
            raise OutputDeviceError.WriteRequestFailedError() from e
 def _showRequestSucceededMessage(self):
     confirmation_message_template = i18n_catalog.i18nc(
         "@info:status",
         "Sent {file_name} to group {cluster_name}."
     )
     file_name = os.path.basename(self._file_name).split(".")[0]
     message_text = confirmation_message_template.format(cluster_name = self.getName(), file_name = file_name)
     message = Message(text=message_text)
     button_text = i18n_catalog.i18nc("@action:button", "Show print jobs")
     button_tooltip = i18n_catalog.i18nc("@info:tooltip", "Opens the print jobs interface in your browser.")
     message.addAction("open_browser", button_text, "globe", button_tooltip)
     message.actionTriggered.connect(self._onMessageActionTriggered)
     message.show()
    def ejectDrive(self, name, path):
        p = subprocess.Popen(["diskutil", "eject", path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        output = p.communicate()

        return_code = p.wait()
        if return_code != 0:
            message = Message("Failed to eject {0}. Maybe it is still in use?".format(name))
            message.show()
            return False
        else:
            message = Message("Ejected {0}. You can now safely remove the card.".format(name))
            message.show()
            return True
Exemple #20
0
    def _installPackage(self, installation_package_data: Dict[str, Any]) -> None:
        package_info = installation_package_data["package_info"]
        filename = installation_package_data["filename"]

        package_id = package_info["package_id"]
        Logger.log("i", "Installing package [%s] from file [%s]", package_id, filename)

        # Load the cached package file and extract all contents to a temporary directory
        if not os.path.exists(filename):
            Logger.log("w", "Package [%s] file '%s' is missing, cannot install this package", package_id, filename)
            return
        try:
            with zipfile.ZipFile(filename, "r") as archive:
                temp_dir = tempfile.TemporaryDirectory()
                archive.extractall(temp_dir.name)
        except Exception:
            Logger.logException("e", "Failed to install package from file [%s]", filename)
            return

        # Remove it first and then install
        try:
            self._purgePackage(package_id)
        except Exception as e:
            message = Message(catalog.i18nc("@error:update",
                                            "There was an error uninstalling the package {package} before installing "
                                            "new version:\n{error}.\nPlease try to upgrade again later.".format(
                                            package = package_id, error = str(e))),
                              title = catalog.i18nc("@info:title", "Updating error"))
            message.show()
            return

        # Copy the folders there
        for sub_dir_name, installation_root_dir in self._installation_dirs_dict.items():
            src_dir_path = os.path.join(temp_dir.name, "files", sub_dir_name)
            dst_dir_path = os.path.join(installation_root_dir, package_id)

            if not os.path.exists(src_dir_path):
                Logger.log("w", "The path %s does not exist, so not installing the files", src_dir_path)
                continue
            self.__installPackageFiles(package_id, src_dir_path, dst_dir_path)

        # Remove the file
        try:
            os.remove(filename)
        except Exception:
            Logger.log("w", "Tried to delete file [%s], but it failed", filename)

        # Move the info to the installed list of packages only when it succeeds
        self._installed_package_dict[package_id] = self._to_install_package_dict[package_id]
        self._installed_package_dict[package_id]["package_info"]["is_installed"] = True
Exemple #21
0
    def run(self):
        super().run()

        if not self._result:
            self._result = []

        # Scale down to maximum bounds size if that is available
        if hasattr(self._application.getController().getScene(), "_maximum_bounds"):
            for node in self._result:
                max_bounds = self._application.getController().getScene()._maximum_bounds
                node._resetAABB()
                build_bounds = node.getBoundingBox()

                if build_bounds is None or max_bounds is None:
                    continue

                if self._application.getInstance().getPreferences().getValue("mesh/scale_to_fit") == True or self._application.getInstance().getPreferences().getValue("mesh/scale_tiny_meshes") == True:
                    scale_factor_width = max_bounds.width / build_bounds.width
                    scale_factor_height = max_bounds.height / build_bounds.height
                    scale_factor_depth = max_bounds.depth / build_bounds.depth
                    scale_factor = min(scale_factor_width, scale_factor_depth, scale_factor_height)
                    if self._application.getInstance().getPreferences().getValue("mesh/scale_to_fit") == True and (scale_factor_width < 1 or scale_factor_height < 1 or scale_factor_depth < 1): # Use scale factor to scale large object down
                        # Ignore scaling on models which are less than 1.25 times bigger than the build volume
                        ignore_factor = 1.25
                        if 1 / scale_factor < ignore_factor:
                            Logger.log("i", "Ignoring auto-scaling, because %.3d < %.3d" % (1 / scale_factor, ignore_factor))
                            scale_factor = 1
                        pass
                    elif self._application.getInstance().getPreferences().getValue("mesh/scale_tiny_meshes") == True and (scale_factor_width > 100 and scale_factor_height > 100 and scale_factor_depth > 100):
                        # Round scale factor to lower factor of 10 to scale tiny object up (eg convert m to mm units)
                        try:
                            scale_factor = math.pow(10, math.floor(math.log(scale_factor) / math.log(10)))
                        except:
                            # In certain cases the scale_factor can be inf which can make this fail. Just use 1 instead.
                            scale_factor = 1
                    else:
                        scale_factor = 1

                    if scale_factor != 1:
                        scale_vector = Vector(scale_factor, scale_factor, scale_factor)
                        display_scale_factor = scale_factor * 100

                        scale_message = Message(i18n_catalog.i18nc("@info:status", "Auto scaled object to {0}% of original size", ("%i" % display_scale_factor)), title = i18n_catalog.i18nc("@info:title", "Scaling Object"))

                        try:
                            node.scale(scale_vector)
                            scale_message.show()
                        except Exception:
                            Logger.logException("e", "While auto-scaling an exception has been raised")
Exemple #22
0
    def importProfile(self, url):
        path = url.toLocalFile()
        if not path:
            return

        profile = Profile(self._manager, read_only = False)
        try:
            profile.loadFromFile(path)
        except Exception as e:
            m = Message(catalog.i18nc("@info:status", "Failed to import profile from file <filename>{0}</filename>: <message>{1}</message>", path, str(e)))
            m.show()
        else:
            m = Message(catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName()))
            m.show()
            self._manager.addProfile(profile)
Exemple #23
0
    def ejectDevice(self, device):
        try:
            Logger.log("i", "Attempting to eject the device")
            result = self.performEjectDevice(device)
        except Exception as e:
            Logger.log("e", "Ejection failed due to: %s" % str(e))
            result = False

        if result:
            Logger.log("i", "Succesfully ejected the device")
            message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(device.getName()))
            message.show()
        else:
            message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Maybe it is still in use?").format(device.getName()))
            message.show()
 def _onWriteJobFinished(self, job):
     self._writing = False
     self.writeFinished.emit(self)
     if job.getResult():
         self.writeSuccess.emit(self)
         message = Message(catalog.i18nc("@info:status", "Saved to <filename>{0}</filename>").format(job.getFileName()))
         message.addAction("open_folder", catalog.i18nc("@action:button", "Open Folder"), "open-folder", catalog.i18nc("@info:tooltip","Open the folder containing the file"))
         message._folder = os.path.dirname(job.getFileName())
         message.actionTriggered.connect(self._onMessageActionTriggered)
         message.show()
     else:
         message = Message(catalog.i18nc("@info:status", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(job.getFileName(), str(job.getError())), lifetime = 0)
         message.show()
         self.writeError.emit(self)
     job.getStream().close()
Exemple #25
0
    def run(self):
        loading_message = Message(i18n_catalog.i18nc("@info:status", "Loading <filename>{0}</filename>", self._filename), lifetime = 0, dismissable = False)
        loading_message.setProgress(-1)
        loading_message.show()

        Job.yieldThread() # Yield to any other thread that might want to do something else.

        try:
            begin_time = time.time()
            node = self._handler.read(self._filename)
            end_time = time.time()
            Logger.log("d", "Loading mesh took %s seconds", end_time - begin_time)
        except Exception as e:
            print(e)
        if not node:
            loading_message.hide()

            result_message = Message(i18n_catalog.i18nc("@info:status", "Failed to load <filename>{0}</filename>", self._filename))
            result_message.show()
            return
        if node.getMeshData():
            node.getMeshData().setFileName(self._filename)
        # Scale down to maximum bounds size if that is available
        if hasattr(Application.getInstance().getController().getScene(), "_maximum_bounds"):
            max_bounds = Application.getInstance().getController().getScene()._maximum_bounds
            node._resetAABB()
            bounding_box = node.getBoundingBox()
            timeout_counter = 0
            #As the calculation of the bounding box is in a seperate thread it might be that it's not done yet.
            while bounding_box.width == 0 or bounding_box.height == 0 or bounding_box.depth == 0:
                bounding_box = node.getBoundingBox()
                time.sleep(0.1)
                timeout_counter += 1
                if timeout_counter > 10:
                    break
            if max_bounds.width < bounding_box.width or max_bounds.height < bounding_box.height or max_bounds.depth < bounding_box.depth:
                scale_factor_width = max_bounds.width / bounding_box.width
                scale_factor_height = max_bounds.height / bounding_box.height
                scale_factor_depth = max_bounds.depth / bounding_box.depth
                scale_factor = min(scale_factor_width,scale_factor_height,scale_factor_depth)

                scale_vector = Vector(scale_factor, scale_factor, scale_factor)
                display_scale_factor = scale_factor * 100

                if Preferences.getInstance().getValue("mesh/scale_to_fit") == True:
                    scale_message = Message(i18n_catalog.i18nc("@info:status", "Auto scaled object to {0}% of original size", ("%i" % display_scale_factor)))

                    try:
                        node.scale(scale_vector)
                        scale_message.show()
                    except Exception as e:
                        print(e)

        self.setResult(node)

        loading_message.hide()
        result_message = Message(i18n_catalog.i18nc("@info:status", "Loaded <filename>{0}</filename>", self._filename))
        result_message.show()
 def _onFinished(self, job):
     if hasattr(job, "_message"):
         job._message.hide()
         job._message = None
     self.writeFinished.emit(self)
     if job.getResult():
         message = Message(catalog.i18nc("", "Saved to Removable Drive {0} as {1}").format(self.getName(), os.path.basename(job.getFileName())))
         message.addAction("eject", catalog.i18nc("", "Eject"), "eject", catalog.i18nc("", "Eject removable device {0}").format(self.getName()))
         message.actionTriggered.connect(self._onActionTriggered)
         message.show()
         self.writeSuccess.emit(self)
     else:
         message = Message(catalog.i18nc("", "Could not save to removable drive {0}: {1}").format(self.getName(), str(job.getError())))
         message.show()
         self.writeError.emit(self)
     job.getStream().close()
    def _writeToDevice(self, node, device_id, file_name):
        device = self._device_manager.getOutputDevice(device_id)
        if not device:
            return

        try:
            if not self._device_manager.isWriteInProgress():
                self._device_manager.setWriteInProgress(True)
                device.requestWrite(node, file_name)
        except OutputDeviceError.UserCanceledError:
            pass
        except OutputDeviceError.DeviceBusyError:
            pass
        except OutputDeviceError.WriteRequestFailedError as e:
            message = Message(str(e))
            message.show()
    def exportProfile(self, name, url):
        path = url.toLocalFile()
        if not path:
            return

        profile = self._manager.findProfile(name)
        if not profile:
            return

        try:
            profile.saveToFile(path)
        except Exception as e:
            m = Message(catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", path, str(e)))
            m.show()
        else:
            m = Message(catalog.i18nc("@info:status", "Exported profile to <filename>{0}</filename>", path))
            m.show()
    def requestWrite(self, node, file_name = None):
        if self._writing:
            raise OutputDeviceError.DeviceBusyError()

        file_type = Preferences.getInstance().getValue("removable_drive/file_type")
        gcode_writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_type)
        if not gcode_writer:
            Logger.log("e", "Could not find writer for MIME type %s, not writing to removable drive %s", file_type, self.getName())
            raise OutputDeviceError.WriteRequestFailedError()

        if file_name == None:
            for n in BreadthFirstIterator(node):
                if n.getMeshData():
                    file_name = n.getName()
                    if file_name:
                        break

        if not file_name:
            Logger.log("e", "Could not determine a proper file name when trying to write to %s, aborting", self.getName())
            raise OutputDeviceError.WriteRequestFailedError()

        file_name = os.path.join(self.getId(), os.path.splitext(file_name)[0] + ".gcode")

        try:
            Logger.log("d", "Writing to %s", file_name)
            stream = open(file_name, "wt")
            job = WriteMeshJob(gcode_writer, stream, node, MeshWriter.OutputMode.TextMode)
            job.setFileName(file_name)
            job.progress.connect(self._onProgress)
            job.finished.connect(self._onFinished)

            message = Message(catalog.i18nc("@info:progress", "Saving to Removable Drive <filename>{0}</filename>").format(self.getName()), 0, False, -1)
            message.show()

            self.writeStarted.emit(self)

            job._message = message
            self._writing = True
            job.start()
        except PermissionError as e:
            Logger.log("e", "Permission denied when trying to write to %s: %s", file_name, str(e))
            raise OutputDeviceError.PermissionDeniedError() from e
        except OSError as e:
            Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e))
            raise OutputDeviceError.WriteRequestFailedError() from e
    def requestWrite(self, node, file_name=None, filter_by_machine = False):
        if self._writing:
            raise OutputDeviceError.DeviceBusyError()

        self.writeStarted.emit(self)
        mesh_writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType("text/x-gcode")

        job = OctoprintUploadJob(mesh_writer, node)
        job.setFileName(file_name + ".gcode")
        job.progress.connect(self._onJobProgress)
        job.finished.connect(self._onWriteJobFinished)

        message = Message("Uploading {0} to {1}".format(job.getFileName(), Preferences.getInstance().getValue("octoprint/base_url")), 0, progress=-1)
        message.show()

        job._message = message
        self._writing = True
        job.start()
Exemple #31
0
class USBPrinterOutputDevice(PrinterOutputDevice):
    def __init__(self, serial_port):
        super().__init__(serial_port)
        self.setName(catalog.i18nc("@item:inmenu", "USB printing"))
        self.setShortDescription(
            catalog.i18nc("@action:button Preceded by 'Ready to'.",
                          "Print via USB"))
        self.setDescription(catalog.i18nc("@info:tooltip", "Print via USB"))
        self.setIconName("print")
        self.setConnectionText(
            catalog.i18nc("@info:status", "Connected via USB"))

        self._serial = None
        self._serial_port = serial_port
        self._error_state = None

        self._connect_thread = threading.Thread(target=self._connect)
        self._connect_thread.daemon = True

        self._end_stop_thread = None
        self._poll_endstop = False

        # The baud checking is done by sending a number of m105 commands to the printer and waiting for a readable
        # response. If the baudrate is correct, this should make sense, else we get giberish.
        self._required_responses_auto_baud = 3

        self._listen_thread = threading.Thread(target=self._listen)
        self._listen_thread.daemon = True

        self._update_firmware_thread = threading.Thread(
            target=self._updateFirmware)
        self._update_firmware_thread.daemon = True
        self.firmwareUpdateComplete.connect(self._onFirmwareUpdateComplete)

        self._heatup_wait_start_time = time.time()

        ## Queue for commands that need to be send. Used when command is sent when a print is active.
        self._command_queue = queue.Queue()

        self._is_printing = False
        self._is_paused = False

        ## Set when print is started in order to check running time.
        self._print_start_time = None
        self._print_start_time_100 = None

        ## Keep track where in the provided g-code the print is
        self._gcode_position = 0

        # List of gcode lines to be printed
        self._gcode = []

        # Check if endstops are ever pressed (used for first run)
        self._x_min_endstop_pressed = False
        self._y_min_endstop_pressed = False
        self._z_min_endstop_pressed = False

        self._x_max_endstop_pressed = False
        self._y_max_endstop_pressed = False
        self._z_max_endstop_pressed = False

        # In order to keep the connection alive we request the temperature every so often from a different extruder.
        # This index is the extruder we requested data from the last time.
        self._temperature_requested_extruder_index = 0

        self._current_z = 0

        self._updating_firmware = False

        self._firmware_file_name = None
        self._firmware_update_finished = False

        self._error_message = None
        self._error_code = 0

    onError = pyqtSignal()

    firmwareUpdateComplete = pyqtSignal()
    firmwareUpdateChange = pyqtSignal()

    endstopStateChanged = pyqtSignal(str, bool, arguments=["key", "state"])

    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 startPrint(self):
        self.writeStarted.emit(self)
        gcode_list = getattr(
            Application.getInstance().getController().getScene(), "gcode_list")
        self._updateJobState("printing")
        self.printGCode(gcode_list)

    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")

    ##  Start a print based on a g-code.
    #   \param gcode_list List with gcode (strings).
    def printGCode(self, gcode_list):
        if self._progress or self._connection_state != ConnectionState.connected:
            self._error_message = Message(
                catalog.i18nc(
                    "@info:status",
                    "Unable to start a new job because the printer is busy or not connected."
                ))
            self._error_message.show()
            Logger.log("d", "Printer is busy or not connected, aborting print")
            self.writeError.emit(self)
            return

        self._gcode.clear()
        for layer in gcode_list:
            self._gcode.extend(layer.split("\n"))

        # Reset line number. If this is not done, first line is sometimes ignored
        self._gcode.insert(0, "M110")
        self._gcode_position = 0
        self._print_start_time_100 = None
        self._is_printing = True
        self._print_start_time = time.time()

        for i in range(
                0, 4):  # Push first 4 entries before accepting other inputs
            self._sendNextGcodeLine()

        self.writeFinished.emit(self)

    ##  Get the serial port string of this connection.
    #   \return serial port
    def getSerialPort(self):
        return self._serial_port

    ##  Try to connect the serial. This simply starts the thread, which runs _connect.
    def connect(self):
        if not self._updating_firmware and not self._connect_thread.isAlive():
            self._connect_thread.start()

    ##  Private function (threaded) that actually uploads the firmware.
    def _updateFirmware(self):
        self._error_code = 0
        self.setProgress(0, 100)
        self._firmware_update_finished = False

        if self._connection_state != ConnectionState.closed:
            self.close()
        hex_file = intelHex.readHex(self._firmware_file_name)

        if len(hex_file) == 0:
            Logger.log(
                "e",
                "Unable to read provided hex file. Could not update firmware")
            self._updateFirmwareFailedMissingFirmware()
            return

        programmer = stk500v2.Stk500v2()
        programmer.progress_callback = self.setProgress

        try:
            programmer.connect(self._serial_port)
        except Exception:
            pass

        # Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases.
        time.sleep(1)

        if not programmer.isConnected():
            Logger.log(
                "e",
                "Unable to connect with serial. Could not update firmware")
            self._updateFirmwareFailedCommunicationError()
            return

        self._updating_firmware = True

        try:
            programmer.programChip(hex_file)
            self._updating_firmware = False
        except serial.SerialException as e:
            Logger.log(
                "e", "SerialException while trying to update firmware: <%s>" %
                (repr(e)))
            self._updateFirmwareFailedIOError()
            return
        except Exception as e:
            Logger.log(
                "e",
                "Exception while trying to update firmware: <%s>" % (repr(e)))
            self._updateFirmwareFailedUnknown()
            return
        programmer.close()

        self._updateFirmwareCompletedSucessfully()
        return

    ##  Private function which makes sure that firmware update process has failed by missing firmware
    def _updateFirmwareFailedMissingFirmware(self):
        return self._updateFirmwareFailedCommon(4)

    ##  Private function which makes sure that firmware update process has failed by an IO error
    def _updateFirmwareFailedIOError(self):
        return self._updateFirmwareFailedCommon(3)

    ##  Private function which makes sure that firmware update process has failed by a communication problem
    def _updateFirmwareFailedCommunicationError(self):
        return self._updateFirmwareFailedCommon(2)

    ##  Private function which makes sure that firmware update process has failed by an unknown error
    def _updateFirmwareFailedUnknown(self):
        return self._updateFirmwareFailedCommon(1)

    ##  Private common function which makes sure that firmware update process has completed/ended with a set progress state
    def _updateFirmwareFailedCommon(self, code):
        if not code:
            raise Exception("Error code not set!")

        self._error_code = code

        self._firmware_update_finished = True
        self.resetFirmwareUpdate(update_has_finished=True)
        self.progressChanged.emit()
        self.firmwareUpdateComplete.emit()

        return

    ##  Private function which makes sure that firmware update process has successfully completed
    def _updateFirmwareCompletedSucessfully(self):
        self.setProgress(100, 100)
        self._firmware_update_finished = True
        self.resetFirmwareUpdate(update_has_finished=True)
        self.firmwareUpdateComplete.emit()

        return

    ##  Upload new firmware to machine
    #   \param filename full path of firmware file to be uploaded
    def updateFirmware(self, file_name):
        Logger.log("i", "Updating firmware of %s using %s", self._serial_port,
                   file_name)
        self._firmware_file_name = file_name
        self._update_firmware_thread.start()

    @property
    def firmwareUpdateFinished(self):
        return self._firmware_update_finished

    def resetFirmwareUpdate(self, update_has_finished=False):
        self._firmware_update_finished = update_has_finished
        self.firmwareUpdateChange.emit()

    @pyqtSlot()
    def startPollEndstop(self):
        if not self._poll_endstop:
            self._poll_endstop = True
            if self._end_stop_thread is None:
                self._end_stop_thread = threading.Thread(
                    target=self._pollEndStop)
                self._end_stop_thread.daemon = True
            self._end_stop_thread.start()

    @pyqtSlot()
    def stopPollEndstop(self):
        self._poll_endstop = False
        self._end_stop_thread = None

    def _pollEndStop(self):
        while self._connection_state == ConnectionState.connected and self._poll_endstop:
            self.sendCommand("M119")
            time.sleep(0.5)

    ##  Private connect function run by thread. Can be started by calling connect.
    def _connect(self):
        Logger.log("d", "Attempting to connect to %s", self._serial_port)
        self.setConnectionState(ConnectionState.connecting)
        programmer = stk500v2.Stk500v2()
        try:
            programmer.connect(
                self._serial_port
            )  # Connect with the serial, if this succeeds, it's an arduino based usb device.
            self._serial = programmer.leaveISP()
        except ispBase.IspError as e:
            Logger.log(
                "i",
                "Could not establish connection on %s: %s. Device is not arduino based."
                % (self._serial_port, str(e)))
        except Exception as e:
            Logger.log(
                "i",
                "Could not establish connection on %s, unknown reasons.  Device is not arduino based."
                % self._serial_port)

        # If the programmer connected, we know its an atmega based version.
        # Not all that useful, but it does give some debugging information.
        for baud_rate in self._getBaudrateList(
        ):  # Cycle all baud rates (auto detect)
            Logger.log(
                "d",
                "Attempting to connect to printer with serial %s on baud rate %s",
                self._serial_port, baud_rate)
            if self._serial is None:
                try:
                    self._serial = serial.Serial(str(self._serial_port),
                                                 baud_rate,
                                                 timeout=3,
                                                 writeTimeout=10000)
                except serial.SerialException:
                    Logger.log("d",
                               "Could not open port %s" % self._serial_port)
                    continue
            else:
                if not self.setBaudRate(baud_rate):
                    continue  # Could not set the baud rate, go to the next

            time.sleep(
                1.5
            )  # Ensure that we are not talking to the bootloader. 1.5 seconds seems to be the magic number
            sucesfull_responses = 0
            timeout_time = time.time() + 5
            self._serial.write(b"\n")
            self._sendCommand(
                "M105"
            )  # Request temperature, as this should (if baudrate is correct) result in a command with "T:" in it
            while timeout_time > time.time():
                line = self._readline()
                if line is None:
                    Logger.log("d",
                               "No response from serial connection received.")
                    # Something went wrong with reading, could be that close was called.
                    self.setConnectionState(ConnectionState.closed)
                    return

                if b"T:" in line:
                    Logger.log(
                        "d",
                        "Correct response for auto-baudrate detection received."
                    )
                    self._serial.timeout = 0.5
                    sucesfull_responses += 1
                    if sucesfull_responses >= self._required_responses_auto_baud:
                        self._serial.timeout = 2  # Reset serial timeout
                        self.setConnectionState(ConnectionState.connected)
                        self._listen_thread.start()  # Start listening
                        Logger.log(
                            "i", "Established printer connection on port %s" %
                            self._serial_port)
                        return

                self._sendCommand(
                    "M105"
                )  # Send M105 as long as we are listening, otherwise we end up in an undefined state

        Logger.log("e", "Baud rate detection for %s failed", self._serial_port)
        self.close()  # Unable to connect, wrap up.
        self.setConnectionState(ConnectionState.closed)

    ##  Set the baud rate of the serial. This can cause exceptions, but we simply want to ignore those.
    def setBaudRate(self, baud_rate):
        try:
            self._serial.baudrate = baud_rate
            return True
        except Exception as e:
            return False

    ##  Close the printer connection
    def close(self):
        Logger.log("d", "Closing the USB printer connection.")
        if self._connect_thread.isAlive():
            try:
                self._connect_thread.join()
            except Exception as e:
                Logger.log("d", "PrinterConnection.close: %s (expected)", e)
                pass  # This should work, but it does fail sometimes for some reason

        self._connect_thread = threading.Thread(target=self._connect)
        self._connect_thread.daemon = True

        self.setConnectionState(ConnectionState.closed)
        if self._serial is not None:
            try:
                self._listen_thread.join()
            except:
                pass
            self._serial.close()

        self._listen_thread = threading.Thread(target=self._listen)
        self._listen_thread.daemon = True
        self._serial = None

    ##  Directly send the command, withouth checking connection state (eg; printing).
    #   \param cmd string with g-code
    def _sendCommand(self, cmd):
        if self._serial is None:
            return

        if "M109" in cmd or "M190" in cmd:
            self._heatup_wait_start_time = time.time()

        try:
            command = (cmd + "\n").encode()
            self._serial.write(b"\n")
            self._serial.write(command)
        except serial.SerialTimeoutException:
            Logger.log(
                "w",
                "Serial timeout while writing to serial port, trying again.")
            try:
                time.sleep(0.5)
                self._serial.write((cmd + "\n").encode())
            except Exception as e:
                Logger.log(
                    "e", "Unexpected error while writing serial port %s " % e)
                self._setErrorState(
                    "Unexpected error while writing serial port %s " % e)
                self.close()
        except Exception as e:
            Logger.log("e",
                       "Unexpected error while writing serial port %s" % e)
            self._setErrorState(
                "Unexpected error while writing serial port %s " % e)
            self.close()

    ##  Send a command to printer.
    #   \param cmd string with g-code
    def sendCommand(self, cmd):
        if self._progress:
            self._command_queue.put(cmd)
        elif self._connection_state == ConnectionState.connected:
            self._sendCommand(cmd)

    ##  Set the error state with a message.
    #   \param error String with the error message.
    def _setErrorState(self, error):
        self._updateJobState("error")
        self._error_state = error
        self.onError.emit()

    ##  Request the current scene to be sent to a USB-connected printer.
    #
    #   \param nodes A collection of scene nodes to send. This is ignored.
    #   \param file_name \type{string} A suggestion for a file name to write.
    #   This is ignored.
    #   \param filter_by_machine Whether to filter MIME types by machine. This
    #   is ignored.
    def requestWrite(self, nodes, file_name=None, filter_by_machine=False):
        Application.getInstance().showPrintMonitor.emit(True)
        self.startPrint()

    def _setEndstopState(self, endstop_key, value):
        if endstop_key == b"x_min":
            if self._x_min_endstop_pressed != value:
                self.endstopStateChanged.emit("x_min", value)
            self._x_min_endstop_pressed = value
        elif endstop_key == b"y_min":
            if self._y_min_endstop_pressed != value:
                self.endstopStateChanged.emit("y_min", value)
            self._y_min_endstop_pressed = value
        elif endstop_key == b"z_min":
            if self._z_min_endstop_pressed != value:
                self.endstopStateChanged.emit("z_min", value)
            self._z_min_endstop_pressed = value

    ##  Listen thread function.
    def _listen(self):
        Logger.log(
            "i", "Printer connection listen thread started for %s" %
            self._serial_port)
        temperature_request_timeout = time.time()
        ok_timeout = time.time()
        while self._connection_state == ConnectionState.connected:
            line = self._readline()
            if line is None:
                break  # None is only returned when something went wrong. Stop listening

            if time.time() > temperature_request_timeout:
                if self._num_extruders > 0:
                    self._temperature_requested_extruder_index = (
                        self._temperature_requested_extruder_index +
                        1) % self._num_extruders
                    self.sendCommand(
                        "M105 T%d" %
                        (self._temperature_requested_extruder_index))
                else:
                    self.sendCommand("M105")
                temperature_request_timeout = time.time() + 5

            if line.startswith(b"Error:"):
                # Oh YEAH, consistency.
                # Marlin reports a MIN/MAX temp error as "Error:x\n: Extruder switched off. MAXTEMP triggered !\n"
                # But a bed temp error is reported as "Error: Temperature heated bed switched off. MAXTEMP triggered !!"
                # So we can have an extra newline in the most common case. Awesome work people.
                if re.match(b"Error:[0-9]\n", line):
                    line = line.rstrip() + self._readline()

                # Skip the communication errors, as those get corrected.
                if b"Extruder switched off" in line or b"Temperature heated bed switched off" in line or b"Something is wrong, please turn off the printer." in line:
                    if not self.hasError():
                        self._setErrorState(line[6:])

            elif b" T:" in line or line.startswith(
                    b"T:"):  # Temperature message
                try:
                    self._setHotendTemperature(
                        self._temperature_requested_extruder_index,
                        float(re.search(b"T: *([0-9\.]*)", line).group(1)))
                except:
                    pass
                if b"B:" in line:  # Check if it's a bed temperature
                    try:
                        self._setBedTemperature(
                            float(re.search(b"B: *([0-9\.]*)", line).group(1)))
                    except Exception as e:
                        pass
                #TODO: temperature changed callback
            elif b"_min" in line or b"_max" in line:
                tag, value = line.split(b":", 1)
                self._setEndstopState(tag,
                                      (b"H" in value or b"TRIGGERED" in value))

            if self._is_printing:
                if line == b"" and time.time() > ok_timeout:
                    line = b"ok"  # Force a timeout (basically, send next command)

                if b"ok" in line:
                    ok_timeout = time.time() + 5
                    if not self._command_queue.empty():
                        self._sendCommand(self._command_queue.get())
                    elif self._is_paused:
                        line = b""  # Force getting temperature as keep alive
                    else:
                        self._sendNextGcodeLine()
                elif b"resend" in line.lower(
                ) or b"rs" in line:  # Because a resend can be asked with "resend" and "rs"
                    try:
                        self._gcode_position = int(
                            line.replace(b"N:",
                                         b" ").replace(b"N", b" ").replace(
                                             b":", b" ").split()[-1])
                    except:
                        if b"rs" in line:
                            self._gcode_position = int(line.split()[1])

            # Request the temperature on comm timeout (every 2 seconds) when we are not printing.)
            if line == b"":
                if self._num_extruders > 0:
                    self._temperature_requested_extruder_index = (
                        self._temperature_requested_extruder_index +
                        1) % self._num_extruders
                    self.sendCommand(
                        "M105 T%d" %
                        self._temperature_requested_extruder_index)
                else:
                    self.sendCommand("M105")

        Logger.log(
            "i", "Printer connection listen thread stopped for %s" %
            self._serial_port)

    ##  Send next Gcode in the gcode list
    def _sendNextGcodeLine(self):
        if self._gcode_position >= len(self._gcode):
            return
        if self._gcode_position == 100:
            self._print_start_time_100 = time.time()
        line = self._gcode[self._gcode_position]

        if ";" in line:
            line = line[:line.find(";")]
        line = line.strip()
        try:
            if line == "M0" or line == "M1":
                line = "M105"  # Don't send the M0 or M1 to the machine, as M0 and M1 are handled as an LCD menu pause.
            if ("G0" in line or "G1" in line) and "Z" in line:
                z = float(re.search("Z([0-9\.]*)", line).group(1))
                if self._current_z != z:
                    self._current_z = z
        except Exception as e:
            Logger.log("e", "Unexpected error with printer connection: %s" % e)
            self._setErrorState("Unexpected error: %s" % e)
        checksum = functools.reduce(
            lambda x, y: x ^ y, map(ord,
                                    "N%d%s" % (self._gcode_position, line)))

        self._sendCommand("N%d%s*%d" % (self._gcode_position, line, checksum))
        self._gcode_position += 1
        self.setProgress((self._gcode_position / len(self._gcode)) * 100)
        self.progressChanged.emit()

    ##  Set the state of the print.
    #   Sent from the print monitor
    def _setJobState(self, job_state):
        if job_state == "pause":
            self._is_paused = True
            self._updateJobState("paused")
        elif job_state == "print":
            self._is_paused = False
            self._updateJobState("printing")
        elif job_state == "abort":
            self.cancelPrint()

    ##  Set the progress of the print.
    #   It will be normalized (based on max_progress) to range 0 - 100
    def setProgress(self, progress, max_progress=100):
        self._progress = (progress /
                          max_progress) * 100  # Convert to scale of 0-100
        if self._progress == 100:
            # Printing is done, reset progress
            self._gcode_position = 0
            self.setProgress(0)
            self._is_printing = False
            self._is_paused = False
            self._updateJobState("ready")
        self.progressChanged.emit()

    ##  Cancel the current print. Printer connection wil continue to listen.
    def cancelPrint(self):
        self._gcode_position = 0
        self.setProgress(0)
        self._gcode = []

        # Turn off temperatures, fan and steppers
        self._sendCommand("M140 S0")
        self._sendCommand("M104 S0")
        self._sendCommand("M107")
        self._sendCommand("M84")
        self._is_printing = False
        self._is_paused = False
        self._updateJobState("ready")
        Application.getInstance().showPrintMonitor.emit(False)

    ##  Check if the process did not encounter an error yet.
    def hasError(self):
        return self._error_state is not None

    ##  private read line used by printer connection to listen for data on serial port.
    def _readline(self):
        if self._serial is None:
            return None
        try:
            ret = self._serial.readline()
        except Exception as e:
            Logger.log("e",
                       "Unexpected error while reading serial port. %s" % e)
            self._setErrorState("Printer has been disconnected")
            self.close()
            return None
        return ret

    ##  Create a list of baud rates at which we can communicate.
    #   \return list of int
    def _getBaudrateList(self):
        ret = [115200, 250000, 230400, 57600, 38400, 19200, 9600]
        return ret

    def _onFirmwareUpdateComplete(self):
        self._update_firmware_thread.join()
        self._update_firmware_thread = threading.Thread(
            target=self._updateFirmware)
        self._update_firmware_thread.daemon = True

        self.connect()
class CuraSnapmakerSenderOutputDevice(
        OutputDevice):  #We need an actual device to do the writing.
    def __init__(self, uri: str, name: str, token=''):
        super().__init__(
            "CuraSnapmakerSenderOutputDevice"
        )  #Give an ID which is used to refer to the output device.
        self._nameofSnapmaker = name
        self._uri = uri
        self._id = uri
        #Optionally set some metadata.
        self.setName("CuraSnapmakerSender")
        self.setShortDescription(
            i18n_catalog.i18nc("@message", "Send To ") +
            self._nameofSnapmaker)  #This is put on the save button.
        self.setDescription(
            i18n_catalog.i18nc("@message", "Send To ") + self._nameofSnapmaker)
        self.setIconName("save")

        self._token = token
        self._printer = SnapmakerApiV1.SnapmakerApiV1(self._uri, self._token)
        self._authrequired_message = Message(i18n_catalog.i18nc(
            "@message",
            "Awaiting Authorization.\r\nPlease allow the connection on your Snapmaker."
        ),
                                             dismissable=False)
        self._authrequired_message.addAction(
            "abort",
            i18n_catalog.i18nc("@button", "Abort"),
            icon="abort",
            description=i18n_catalog.i18nc("@message:description",
                                           "Abort Sending..."))
        self._authrequired_message.actionTriggered.connect(self.abortSend)
        self._connect_failed_message = Message(i18n_catalog.i18nc(
            "@message",
            "Could not connect to your machine. It is either off, the given address is wrong or not reachable or your Snapmaker refused the connection."
        ),
                                               lifetime=30,
                                               dismissable=True,
                                               title='Error')
        self._prepare_send_message = Message(i18n_catalog.i18nc(
            "@message", "Preparing Gcode for sending, please wait."),
                                             dismissable=True,
                                             title='Info')
        self._progress_message = Message(
            i18n_catalog.i18nc("@message", "Sending file to ") +
            self._nameofSnapmaker)

    def requestWrite(self,
                     nodes,
                     file_name=None,
                     limit_mimetypes=None,
                     file_handler=None,
                     **kwargs):
        #Logger.log("d","Firing Timer")
        self._writeHandleTimer = QTimer()
        self._writeHandleTimer.timeout.connect(lambda: self.handleWrite(
            nodes, file_name, limit_mimetypes, file_handler))
        self._writeHandleTimer.setInterval(1)
        self._writeHandleTimer.setSingleShot(True)
        self._writeHandleTimer.start()

    def abortSend(self, message, action):
        message.hide()
        if (self._printer.state !=
                SnapmakerApiV1.SnapmakerApiState.NOTCONNECTED or
                self._printer.state != SnapmakerApiV1.SnapmakerApiState.FATAL):
            self._printer.disconnect()
        self._writeHandleTimer.stop()

    def handleWrite(self,
                    nodes,
                    file_name=None,
                    limit_mimetypes=None,
                    file_handler=None,
                    **kwargs):
        #Logger.log("d","In handleWrite")
        self._writeHandleTimer.setInterval(1000)
        result = None
        if not self._printer.state == SnapmakerApiV1.SnapmakerApiState.IDLE:
            if (self._printer.state ==
                    SnapmakerApiV1.SnapmakerApiState.NOTCONNECTED):
                result = self._printer.connect()
                if result == False:
                    self.writeError.emit()
                    self._connect_failed_message.show()
                    return
            elif (self._printer.state == SnapmakerApiV1.SnapmakerApiState.FATAL
                  ):
                #Logger.log("d",self._printer.state)
                self._printer = SnapmakerApiV1.SnapmakerApiV1(
                    self._uri, self._printer.token)
                result = self._printer.connect()
                if result == False:
                    self.writeError.emit()
                    self._connect_failed_message.show()
                    return
            elif (self._printer.state ==
                  SnapmakerApiV1.SnapmakerApiState.AWAITING_AUTHORIZATION):
                #Logger.log("d",self._printer.state)
                self._authrequired_message.show()
            else:
                #Logger.log("d",self._printer.state)
                self.writeError.emit()
                message = Message(i18n_catalog.i18nc(
                    "@message", "Sending failed, try again later"),
                                  lifetime=30,
                                  dismissable=True,
                                  title='Error')
                message.show()
                return

            self._writeHandleTimer.start()
            return
        #Logger.log("d","Ready to send")
        self._authrequired_message.hide()
        self._prepare_send_message.show()
        self._token = self._printer.token
        self.writeStarted.emit(self)
        print_info = CuraApplication.getInstance().getPrintInformation()
        gcode_writer = MeshWriter()
        self._gcode_stream = StringIO()
        #In case the Plugin Gcodewriter is a separate Plugin
        #try:
        #gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("CuraSnapmakerSender"))
        #except UM.PluginError.PluginNotFoundError:
        #gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
        gcode_writer = SnapmakerGCodeWriter.SnapmakerGCodeWriter()
        if not gcode_writer.write(self._gcode_stream, None):
            #Logger.log("e", "GCodeWrite failed: %s" % gcode_writer.getInformation())
            return
        self.content_length = self._gcode_stream.tell()
        self._gcode_stream.seek(0)
        self._byteStream = BytesIOWrapper(self._gcode_stream)
        self._printer.setBlocking(False)
        self.active_sending_future = self._printer.send_gcode_file(
            print_info.jobName.strip() + ".gcode",
            self._byteStream,
            callback=self.updateProgress)
        self.active_sending_future.add_done_callback(self.transmitDone)
        self._printer.setBlocking(True)
        self._progress_message.setMaxProgress(100)
        self._progress_message.setProgress(0)
        self._progress_message.show()
        #Logger.log("d","WriteRequested")

    def updateProgress(self, monitor):
        ##Logger.log("d",str(monitor.bytes_read) +" of " + str(self.content_length))
        self._prepare_send_message.hide()
        self._progress_message.setProgress(
            (monitor.bytes_read / self.content_length) * 100)
        self.writeProgress.emit()

    def transmitDone(self, future):
        #Logger.log("d","WriteDone")
        self._progress_message.hide()
        if self.active_sending_future.result():
            self.writeFinished.emit()
            self.writeSuccess.emit()
        else:
            self.writeError.emit()

    def tearDown(self):
        self._printer.disconnect()
Exemple #33
0
    def run(self):
        status_message = Message(
            i18n_catalog.i18nc("@info:status",
                               "Multiplying and placing objects"),
            lifetime=0,
            dismissable=False,
            progress=0,
            title=i18n_catalog.i18nc("@info:title", "Placing Object"))
        status_message.show()
        scene = Application.getInstance().getController().getScene()

        total_progress = len(self._objects) * self._count
        current_progress = 0

        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        machine_width = global_container_stack.getProperty(
            "machine_width", "value")
        machine_depth = global_container_stack.getProperty(
            "machine_depth", "value")

        root = scene.getRoot()
        scale = 0.5
        arranger = Arrange.create(x=machine_width,
                                  y=machine_depth,
                                  scene_root=root,
                                  scale=scale,
                                  min_offset=self._min_offset)
        processed_nodes = []
        nodes = []

        not_fit_count = 0

        for node in self._objects:
            # If object is part of a group, multiply group
            current_node = node
            while current_node.getParent() and (
                    current_node.getParent().callDecoration("isGroup")
                    or current_node.getParent().callDecoration("isSliceable")):
                current_node = current_node.getParent()

            if current_node in processed_nodes:
                continue
            processed_nodes.append(current_node)

            node_too_big = False
            if node.getBoundingBox(
            ).width < machine_width or node.getBoundingBox(
            ).depth < machine_depth:
                offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(
                    current_node, min_offset=self._min_offset, scale=scale)
            else:
                node_too_big = True

            found_solution_for_all = True
            arranger.resetLastPriority()
            for i in range(self._count):
                # We do place the nodes one by one, as we want to yield in between.
                new_node = copy.deepcopy(node)
                solution_found = False
                if not node_too_big:
                    solution_found = arranger.findNodePlacement(
                        new_node, offset_shape_arr, hull_shape_arr)

                if node_too_big or not solution_found:
                    found_solution_for_all = False
                    new_location = new_node.getPosition()
                    new_location = new_location.set(z=-not_fit_count * 20)
                    new_node.setPosition(new_location)
                    not_fit_count += 1

                # Same build plate
                build_plate_number = current_node.callDecoration(
                    "getBuildPlateNumber")
                new_node.callDecoration("setBuildPlateNumber",
                                        build_plate_number)
                for child in new_node.getChildren():
                    child.callDecoration("setBuildPlateNumber",
                                         build_plate_number)

                nodes.append(new_node)
                current_progress += 1
                status_message.setProgress(
                    (current_progress / total_progress) * 100)
                Job.yieldThread()

            Job.yieldThread()

        if nodes:
            op = GroupedOperation()
            for new_node in nodes:
                op.addOperation(
                    AddSceneNodeOperation(new_node, current_node.getParent()))
            op.push()
        status_message.hide()

        if not found_solution_for_all:
            no_full_solution_message = Message(i18n_catalog.i18nc(
                "@info:status",
                "Unable to find a location within the build volume for all objects"
            ),
                                               title=i18n_catalog.i18nc(
                                                   "@info:title",
                                                   "Placing Object"))
            no_full_solution_message.show()
Exemple #34
0
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

        # 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()
        )).encode()

        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")

        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)

        ##  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 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._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

    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("octoprint_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 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)
        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 OctoPrint on {0}").format(self._key))

        ## 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._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("octoprint_auto_print", True))

        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."))
            elif self._auto_print:
                self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is busy. Unable to start a new job."))
            else:
                # allow queueing the job even if OctoPrint 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 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 self._sd_supported and parseBool(global_container_stack.getMetaDataEntry("octoprint_store_sd", False)):
                destination = "sdcard"

            ##  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)

            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, 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 OctoPrint 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 OctoPrint 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")

    def _extrude(self, e, speed):
        self._sendCommand("G91")
        self._sendCommand("G0 E%s F%s" % (e, speed))
        self._sendCommand("G90")

    def _setHotend(self, num):
        self._sendCommand("T%i" % num)


    def _setZOffset(self, zOffset, saveEEPROM):
        self.sendCommand("M851 Z%s" % (zOffset))
        if saveEEPROM == True:
            self.sendCommand("M500")


    def _getZOffset(self):
        self.sendCommand("M851")
        return self._ZOffset

    ##  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)
                    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._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):
                            if ("tool%d" % index) in json_data["temperature"]:
                                hotend_temperatures = json_data["temperature"]["tool%d" % index]
                                self._setHotendTemperature(index, hotend_temperatures["actual"])
                                self._updateTargetHotendTemperature(index, hotend_temperatures["target"])
                            else:
                                self._setHotendTemperature(index, 0)
                                self._updateTargetHotendTemperature(index, 0)

                        if "bed" in json_data["temperature"]:
                            bed_temperatures = json_data["temperature"]["bed"]
                            self._setBedTemperature(bed_temperatures["actual"])
                            self._updateTargetBedTemperature(bed_temperatures["target"])
                        else:
                            self._setBedTemperature(0)
                            self._updateTargetBedTemperature(0)

                    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:
                    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 = {}

                    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 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._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()

        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 #35
0
    def _installPackage(self, installation_package_data: Dict[str,
                                                              Any]) -> None:
        package_info = installation_package_data["package_info"]
        filename = installation_package_data["filename"]

        package_id = package_info["package_id"]
        Logger.log("i", "Installing package [%s] from file [%s]", package_id,
                   filename)

        # Load the cached package file and extract all contents to a temporary directory
        if not os.path.exists(filename):
            Logger.log(
                "w",
                "Package [%s] file '%s' is missing, cannot install this package",
                package_id, filename)
            return
        try:
            with zipfile.ZipFile(filename, "r") as archive:
                temp_dir = tempfile.TemporaryDirectory()
                archive.extractall(temp_dir.name)
        except Exception:
            Logger.logException("e",
                                "Failed to install package from file [%s]",
                                filename)
            return

        # Remove it first and then install
        try:
            self._purgePackage(package_id)
        except Exception as e:
            message = Message(catalog.i18nc(
                "@error:update",
                "There was an error uninstalling the package {package} before installing "
                "new version:\n{error}.\nPlease try to upgrade again later.".
                format(package=package_id, error=str(e))),
                              title=catalog.i18nc("@info:title",
                                                  "Updating error"))
            message.show()
            return

        # Copy the folders there
        for sub_dir_name, installation_root_dir in self._installation_dirs_dict.items(
        ):
            src_dir_path = os.path.join(temp_dir.name, "files", sub_dir_name)
            dst_dir_path = os.path.join(installation_root_dir, package_id)

            if not os.path.exists(src_dir_path):
                Logger.log(
                    "w",
                    "The path %s does not exist, so not installing the files",
                    src_dir_path)
                continue
            self.__installPackageFiles(package_id, src_dir_path, dst_dir_path)

        # Remove the file
        try:
            os.remove(filename)
        except Exception:
            Logger.log("w", "Tried to delete file [%s], but it failed",
                       filename)

        # Move the info to the installed list of packages only when it succeeds
        self._installed_package_dict[
            package_id] = self._to_install_package_dict[package_id]
        self._installed_package_dict[package_id]["package_info"][
            "is_installed"] = True
Exemple #36
0
class SolidView(View):
    _show_xray_warning_preference = "view/show_xray_warning"

    def __init__(self):
        super().__init__()
        application = Application.getInstance()
        application.getPreferences().addPreference("view/show_overhang", True)
        application.globalContainerStackChanged.connect(
            self._onGlobalContainerChanged)
        self._enabled_shader = None
        self._disabled_shader = None
        self._non_printing_shader = None
        self._support_mesh_shader = None

        self._xray_shader = None
        self._xray_pass = None
        self._xray_composite_shader = None
        self._composite_pass = None

        self._extruders_model = None
        self._theme = None
        self._support_angle = 90

        self._global_stack = None

        self._old_composite_shader = None
        self._old_layer_bindings = None

        self._next_xray_checking_time = time.time()
        self._xray_checking_update_time = 1.0  # seconds
        self._xray_warning_cooldown = 60 * 10  # reshow Model error message every 10 minutes
        self._xray_warning_message = Message(
            catalog.i18nc(
                "@info:status",
                "Your model is not manifold. The highlighted areas indicate either missing or extraneous surfaces."
            ),
            lifetime=60 * 5,  # leave message for 5 minutes
            title=catalog.i18nc("@info:title", "Model errors"),
        )
        application.getPreferences().addPreference(
            self._show_xray_warning_preference, True)

        application.engineCreatedSignal.connect(self._onGlobalContainerChanged)

    def _onGlobalContainerChanged(self) -> None:
        if self._global_stack:
            try:
                self._global_stack.propertyChanged.disconnect(
                    self._onPropertyChanged)
            except TypeError:
                pass
            for extruder_stack in ExtruderManager.getInstance(
            ).getActiveExtruderStacks():
                extruder_stack.propertyChanged.disconnect(
                    self._onPropertyChanged)

        self._global_stack = Application.getInstance().getGlobalContainerStack(
        )
        if self._global_stack:
            self._global_stack.propertyChanged.connect(self._onPropertyChanged)
            for extruder_stack in ExtruderManager.getInstance(
            ).getActiveExtruderStacks():
                extruder_stack.propertyChanged.connect(self._onPropertyChanged)
            self._onPropertyChanged("support_angle",
                                    "value")  # Force an re-evaluation

    def _onPropertyChanged(self, key: str, property_name: str) -> None:
        if key != "support_angle" or property_name != "value":
            return
        # As the rendering is called a *lot* we really, dont want to re-evaluate the property every time. So we store em!
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack:
            support_extruder_nr = int(
                global_container_stack.getExtruderPositionValueWithDefault(
                    "support_extruder_nr"))
            try:
                support_angle_stack = global_container_stack.extruderList[
                    support_extruder_nr]
            except IndexError:
                pass
            else:
                self._support_angle = support_angle_stack.getProperty(
                    "support_angle", "value")

    def _checkSetup(self):
        if not self._extruders_model:
            self._extruders_model = Application.getInstance(
            ).getExtrudersModel()

        if not self._theme:
            self._theme = Application.getInstance().getTheme()

        if not self._enabled_shader:
            self._enabled_shader = OpenGL.getInstance().createShaderProgram(
                Resources.getPath(Resources.Shaders, "overhang.shader"))
            self._enabled_shader.setUniformValue(
                "u_overhangColor",
                Color(*self._theme.getColor("model_overhang").getRgb()))

        if not self._disabled_shader:
            self._disabled_shader = OpenGL.getInstance().createShaderProgram(
                Resources.getPath(Resources.Shaders, "striped.shader"))
            self._disabled_shader.setUniformValue(
                "u_diffuseColor1",
                Color(*self._theme.getColor("model_unslicable").getRgb()))
            self._disabled_shader.setUniformValue(
                "u_diffuseColor2",
                Color(*self._theme.getColor("model_unslicable_alt").getRgb()))
            self._disabled_shader.setUniformValue("u_width", 50.0)

        if not self._non_printing_shader:
            self._non_printing_shader = OpenGL.getInstance(
            ).createShaderProgram(
                Resources.getPath(Resources.Shaders,
                                  "transparent_object.shader"))
            self._non_printing_shader.setUniformValue(
                "u_diffuseColor",
                Color(*self._theme.getColor("model_non_printing").getRgb()))
            self._non_printing_shader.setUniformValue("u_opacity", 0.6)

        if not self._support_mesh_shader:
            self._support_mesh_shader = OpenGL.getInstance(
            ).createShaderProgram(
                Resources.getPath(Resources.Shaders, "striped.shader"))
            self._support_mesh_shader.setUniformValue("u_vertical_stripes",
                                                      True)
            self._support_mesh_shader.setUniformValue("u_width", 5.0)

        if not Application.getInstance().getPreferences().getValue(
                self._show_xray_warning_preference):
            self._xray_shader = None
            self._xray_composite_shader = None
            if self._composite_pass and 'xray' in self._composite_pass.getLayerBindings(
            ):
                self._composite_pass.setLayerBindings(self._old_layer_bindings)
                self._composite_pass.setCompositeShader(
                    self._old_composite_shader)
                self._old_layer_bindings = None
                self._old_composite_shader = None
                self._xray_warning_message.hide()
        else:
            if not self._xray_shader:
                self._xray_shader = OpenGL.getInstance().createShaderProgram(
                    Resources.getPath(Resources.Shaders, "xray.shader"))

            if not self._xray_composite_shader:
                self._xray_composite_shader = OpenGL.getInstance(
                ).createShaderProgram(
                    Resources.getPath(Resources.Shaders,
                                      "xray_composite.shader"))
                theme = Application.getInstance().getTheme()
                self._xray_composite_shader.setUniformValue(
                    "u_background_color",
                    Color(*theme.getColor("viewport_background").getRgb()))
                self._xray_composite_shader.setUniformValue(
                    "u_outline_color",
                    Color(*theme.getColor("model_selection_outline").getRgb()))

            renderer = self.getRenderer()
            if not self._composite_pass or not 'xray' in self._composite_pass.getLayerBindings(
            ):
                # Currently the RenderPass constructor requires a size > 0
                # This should be fixed in RenderPass's constructor.
                self._xray_pass = XRayPass.XRayPass(1, 1)

                renderer.addRenderPass(self._xray_pass)

                if not self._composite_pass:
                    self._composite_pass = self.getRenderer().getRenderPass(
                        "composite")

                self._old_layer_bindings = self._composite_pass.getLayerBindings(
                )
                self._composite_pass.setLayerBindings(
                    ["default", "selection", "xray"])
                self._old_composite_shader = self._composite_pass.getCompositeShader(
                )
                self._composite_pass.setCompositeShader(
                    self._xray_composite_shader)

    def beginRendering(self):
        scene = self.getController().getScene()
        renderer = self.getRenderer()

        self._checkSetup()

        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack:
            if Application.getInstance().getPreferences().getValue(
                    "view/show_overhang"):
                # Make sure the overhang angle is valid before passing it to the shader
                if self._support_angle is not None and self._support_angle >= 0 and self._support_angle <= 90:
                    self._enabled_shader.setUniformValue(
                        "u_overhangAngle",
                        math.cos(math.radians(90 - self._support_angle)))
                else:
                    self._enabled_shader.setUniformValue(
                        "u_overhangAngle", math.cos(math.radians(0))
                    )  #Overhang angle of 0 causes no area at all to be marked as overhang.
            else:
                self._enabled_shader.setUniformValue("u_overhangAngle",
                                                     math.cos(math.radians(0)))

        for node in DepthFirstIterator(scene.getRoot()):
            if not node.render(renderer):
                if node.getMeshData() and node.isVisible(
                ) and not node.callDecoration("getLayerData"):
                    uniforms = {}
                    shade_factor = 1.0

                    per_mesh_stack = node.callDecoration("getStack")

                    extruder_index = node.callDecoration(
                        "getActiveExtruderPosition")
                    if extruder_index is None:
                        extruder_index = "0"
                    extruder_index = int(extruder_index)

                    # Use the support extruder instead of the active extruder if this is a support_mesh
                    if per_mesh_stack:
                        if per_mesh_stack.getProperty("support_mesh", "value"):
                            extruder_index = int(
                                global_container_stack.
                                getExtruderPositionValueWithDefault(
                                    "support_extruder_nr"))

                    try:
                        material_color = self._extruders_model.getItem(
                            extruder_index)["color"]
                    except KeyError:
                        material_color = self._extruders_model.defaultColors[0]

                    if extruder_index != ExtruderManager.getInstance(
                    ).activeExtruderIndex:
                        # Shade objects that are printed with the non-active extruder 25% darker
                        shade_factor = 0.6

                    try:
                        # Colors are passed as rgb hex strings (eg "#ffffff"), and the shader needs
                        # an rgba list of floats (eg [1.0, 1.0, 1.0, 1.0])
                        uniforms["diffuse_color"] = [
                            shade_factor * int(material_color[1:3], 16) / 255,
                            shade_factor * int(material_color[3:5], 16) / 255,
                            shade_factor * int(material_color[5:7], 16) / 255,
                            1.0
                        ]

                        # Color the currently selected face-id. (Disable for now.)
                        #face = Selection.getHoverFace()
                        uniforms[
                            "hover_face"] = -1  #if not face or node != face[0] else face[1]
                    except ValueError:
                        pass

                    if node.callDecoration("isNonPrintingMesh"):
                        if per_mesh_stack and (per_mesh_stack.getProperty(
                                "infill_mesh", "value")
                                               or per_mesh_stack.getProperty(
                                                   "cutting_mesh", "value")):
                            renderer.queueNode(
                                node,
                                shader=self._non_printing_shader,
                                uniforms=uniforms,
                                transparent=True)
                        else:
                            renderer.queueNode(
                                node,
                                shader=self._non_printing_shader,
                                transparent=True)
                    elif getattr(node, "_outside_buildarea", False):
                        renderer.queueNode(node, shader=self._disabled_shader)
                    elif per_mesh_stack and per_mesh_stack.getProperty(
                            "support_mesh", "value"):
                        # Render support meshes with a vertical stripe that is darker
                        shade_factor = 0.6
                        uniforms["diffuse_color_2"] = [
                            uniforms["diffuse_color"][0] * shade_factor,
                            uniforms["diffuse_color"][1] * shade_factor,
                            uniforms["diffuse_color"][2] * shade_factor, 1.0
                        ]
                        renderer.queueNode(node,
                                           shader=self._support_mesh_shader,
                                           uniforms=uniforms)
                    else:
                        renderer.queueNode(node,
                                           shader=self._enabled_shader,
                                           uniforms=uniforms)
                if node.callDecoration("isGroup") and Selection.isSelected(
                        node):
                    renderer.queueNode(scene.getRoot(),
                                       mesh=node.getBoundingBoxMesh(),
                                       mode=RenderBatch.RenderMode.LineLoop)

    def endRendering(self):
        # check whether the xray overlay is showing badness
        if time.time() > self._next_xray_checking_time\
                and Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference):
            self._next_xray_checking_time = time.time(
            ) + self._xray_checking_update_time

            xray_img = self._xray_pass.getOutput()
            xray_img = xray_img.convertToFormat(QImage.Format_RGB888)

            # We can't just read the image since the pixels are aligned to internal memory positions.
            # xray_img.byteCount() != xray_img.width() * xray_img.height() * 3
            # The byte count is a little higher sometimes. We need to check the data per line, but fast using Numpy.
            # See https://stackoverflow.com/questions/5810970/get-raw-data-from-qimage for a description of the problem.
            # We can't use that solution though, since it doesn't perform well in Python.
            class QImageArrayView:
                """
                Class that ducktypes to be a Numpy ndarray.
                """
                def __init__(self, qimage):
                    bits_pointer = qimage.bits()
                    if bits_pointer is None:  # If this happens before there is a window.
                        self.__array_interface__ = {
                            "shape": (0, 0),
                            "typestr": "|u4",
                            "data": (0, False),
                            "strides": (1, 3),
                            "version": 3
                        }
                    else:
                        self.__array_interface__ = {
                            "shape": (qimage.height(), qimage.width()),
                            "typestr":
                            "|u4",  # Use 4 bytes per pixel rather than 3, since Numpy doesn't support 3.
                            "data": (int(bits_pointer), False),
                            "strides": (
                                qimage.bytesPerLine(), 3
                            ),  # This does the magic: For each line, skip the correct number of bytes. Bytes per pixel is always 3 due to QImage.Format.Format_RGB888.
                            "version": 3
                        }

            array = np.asarray(QImageArrayView(xray_img)).view(
                np.dtype({
                    "r": (np.uint8, 0, "red"),
                    "g": (np.uint8, 1, "green"),
                    "b": (np.uint8, 2, "blue"),
                    "a":
                    (np.uint8, 3, "alpha"
                     )  # Never filled since QImage was reformatted to RGB888.
                }),
                np.recarray)
            if np.any(np.mod(array.r, 2)):
                self._next_xray_checking_time = time.time(
                ) + self._xray_warning_cooldown
                self._xray_warning_message.show()
                Logger.log("i", "X-Ray overlay found non-manifold pixels.")

    def event(self, event):
        if event.type == Event.ViewDeactivateEvent:
            if self._composite_pass and 'xray' in self._composite_pass.getLayerBindings(
            ):
                self.getRenderer().removeRenderPass(self._xray_pass)
                self._composite_pass.setLayerBindings(self._old_layer_bindings)
                self._composite_pass.setCompositeShader(
                    self._old_composite_shader)
                self._xray_warning_message.hide()
Exemple #37
0
class CuraEngineBackend(QObject, Backend):
    ##  Starts the back-end plug-in.
    #
    #   This registers all the signal listeners and prepares for communication
    #   with the back-end in general.
    #   CuraEngineBackend is exposed to qml as well.
    def __init__(self, parent = None):
        super().__init__(parent = parent)
        # Find out where the engine is located, and how it is called.
        # This depends on how Cura is packaged and which OS we are running on.
        executable_name = "CuraEngine"
        if Platform.isWindows():
            executable_name += ".exe"
        default_engine_location = executable_name
        if os.path.exists(os.path.join(Application.getInstallPrefix(), "bin", executable_name)):
            default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", executable_name)
        if hasattr(sys, "frozen"):
            default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name)
        if Platform.isLinux() and not default_engine_location:
            if not os.getenv("PATH"):
                raise OSError("There is something wrong with your Linux installation.")
            for pathdir in os.getenv("PATH").split(os.pathsep):
                execpath = os.path.join(pathdir, executable_name)
                if os.path.exists(execpath):
                    default_engine_location = execpath
                    break

        if not default_engine_location:
            raise EnvironmentError("Could not find CuraEngine")

        Logger.log("i", "Found CuraEngine at: %s" %(default_engine_location))

        default_engine_location = os.path.abspath(default_engine_location)
        Preferences.getInstance().addPreference("backend/location", default_engine_location)

        # Workaround to disable layer view processing if layer view is not active.
        self._layer_view_active = False
        Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
        self._onActiveViewChanged()
        self._stored_layer_data = []
        self._stored_optimized_layer_data = []

        self._scene = Application.getInstance().getController().getScene()
        self._scene.sceneChanged.connect(self._onSceneChanged)

        # Triggers for auto-slicing. Auto-slicing is triggered as follows:
        #  - auto-slicing is started with a timer
        #  - whenever there is a value change, we start the timer
        #  - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the
        #    auto-slicing timer when that error check is finished
        #  If there is an error check, it will set the "_is_error_check_scheduled" flag, stop the auto-slicing timer,
        #  and only wait for the error check to be finished to start the auto-slicing timer again.
        #
        self._global_container_stack = None
        Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
        self._onGlobalStackChanged()

        Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)

        # A flag indicating if an error check was scheduled
        # If so, we will stop the auto-slice timer and start upon the error check
        self._is_error_check_scheduled = False

        # Listeners for receiving messages from the back-end.
        self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
        self._message_handlers["cura.proto.LayerOptimized"] = self._onOptimizedLayerMessage
        self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
        self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
        self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
        self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
        self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage

        self._start_slice_job = None
        self._slicing = False  # Are we currently slicing?
        self._restart = False  # Back-end is currently restarting?
        self._tool_active = False  # If a tool is active, some tasks do not have to do anything
        self._always_restart = True  # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
        self._process_layers_job = None  # The currently active job to process layers, or None if it is not processing layers.
        self._need_slicing = False
        self._engine_is_fresh = True  # Is the newly started engine used before or not?

        self._backend_log_max_lines = 20000  # Maximum number of lines to buffer
        self._error_message = None  # Pop-up message that shows errors.
        self._last_num_objects = 0  # Count number of objects to see if there is something changed
        self._postponed_scene_change_sources = []  # scene change is postponed (by a tool)

        self.backendQuit.connect(self._onBackendQuit)
        self.backendConnected.connect(self._onBackendConnected)

        # When a tool operation is in progress, don't slice. So we need to listen for tool operations.
        Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted)
        Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped)

        self._slice_start_time = None

        Preferences.getInstance().addPreference("general/auto_slice", True)

        self._use_timer = False
        # When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
        # This timer will group them up, and only slice for the last setting changed signal.
        # TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
        self._change_timer = QTimer()
        self._change_timer.setSingleShot(True)
        self._change_timer.setInterval(500)
        self.determineAutoSlicing()
        Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)

    ##  Terminate the engine process.
    #
    #   This function should terminate the engine process.
    #   Called when closing the application.
    def close(self):
        # Terminate CuraEngine if it is still running at this point
        self._terminate()

    ##  Get the command that is used to call the engine.
    #   This is useful for debugging and used to actually start the engine.
    #   \return list of commands and args / parameters.
    def getEngineCommand(self):
        json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json")
        return [Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]

    ##  Emitted when we get a message containing print duration and material amount.
    #   This also implies the slicing has finished.
    #   \param time The amount of time the print will take.
    #   \param material_amount The amount of material the print will use.
    printDurationMessage = Signal()

    ##  Emitted when the slicing process starts.
    slicingStarted = Signal()

    ##  Emitted when the slicing process is aborted forcefully.
    slicingCancelled = Signal()

    @pyqtSlot()
    def stopSlicing(self):
        self.backendStateChange.emit(BackendState.NotStarted)
        if self._slicing:  # We were already slicing. Stop the old job.
            self._terminate()
            self._createSocket()

        if self._process_layers_job:  # We were processing layers. Stop that, the layers are going to change soon.
            self._process_layers_job.abort()
            self._process_layers_job = None

        if self._error_message:
            self._error_message.hide()

    ##  Manually triggers a reslice
    @pyqtSlot()
    def forceSlice(self):
        if self._use_timer:
            self._change_timer.start()
        else:
            self.slice()

    ##  Perform a slice of the scene.
    def slice(self):
        self._slice_start_time = time()
        if not self._need_slicing:
            self.processingProgress.emit(1.0)
            self.backendStateChange.emit(BackendState.Done)
            Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
            return
        if Application.getInstance().getPrintInformation():
            Application.getInstance().getPrintInformation().setToZeroPrintInformation()

        self._stored_layer_data = []
        self._stored_optimized_layer_data = []

        if self._process is None:
            self._createSocket()
        self.stopSlicing()
        self._engine_is_fresh = False  # Yes we're going to use the engine

        self.processingProgress.emit(0.0)
        self.backendStateChange.emit(BackendState.NotStarted)

        self._scene.gcode_list = []
        self._slicing = True
        self.slicingStarted.emit()

        slice_message = self._socket.createMessage("cura.proto.Slice")
        self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
        self._start_slice_job.start()
        self._start_slice_job.finished.connect(self._onStartSliceCompleted)

    ##  Terminate the engine process.
    #   Start the engine process by calling _createSocket()
    def _terminate(self):
        self._slicing = False
        self._stored_layer_data = []
        self._stored_optimized_layer_data = []
        if self._start_slice_job is not None:
            self._start_slice_job.cancel()

        self.slicingCancelled.emit()
        self.processingProgress.emit(0)
        Logger.log("d", "Attempting to kill the engine process")

        if Application.getInstance().getCommandLineOption("external-backend", False):
            return

        if self._process is not None:
            Logger.log("d", "Killing engine process")
            try:
                self._process.terminate()
                Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait())
                self._process = None

            except Exception as e:  # terminating a process that is already terminating causes an exception, silently ignore this.
                Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e))

    ##  Event handler to call when the job to initiate the slicing process is
    #   completed.
    #
    #   When the start slice job is successfully completed, it will be happily
    #   slicing. This function handles any errors that may occur during the
    #   bootstrapping of a slice job.
    #
    #   \param job The start slice job that was just finished.
    def _onStartSliceCompleted(self, job):
        if self._error_message:
            self._error_message.hide()

        # Note that cancelled slice jobs can still call this method.
        if self._start_slice_job is job:
            self._start_slice_job = None

        if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
            return

        if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
            if Application.getInstance().platformActivity:
                self._error_message = Message(catalog.i18nc("@info:status",
                                            "Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
                self._error_message.show()
                self.backendStateChange.emit(BackendState.Error)
            else:
                self.backendStateChange.emit(BackendState.NotStarted)
            return

        if job.getResult() == StartSliceJob.StartJobResult.SettingError:
            if Application.getInstance().platformActivity:
                extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
                error_keys = []
                for extruder in extruders:
                    error_keys.extend(extruder.getErrorKeys())
                if not extruders:
                    error_keys = self._global_container_stack.getErrorKeys()
                error_labels = set()
                for key in error_keys:
                    for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack.
                        definitions = stack.getBottom().findDefinitions(key = key)
                        if definitions:
                            break #Found it! No need to continue search.
                    else: #No stack has a definition for this setting.
                        Logger.log("w", "When checking settings for errors, unable to find definition for key: {key}".format(key = key))
                        continue
                    error_labels.add(definitions[0].label)

                error_labels = ", ".join(error_labels)
                self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(error_labels),
                                              title = catalog.i18nc("@info:title", "Unable to slice"))
                self._error_message.show()
                self.backendStateChange.emit(BackendState.Error)
            else:
                self.backendStateChange.emit(BackendState.NotStarted)
            return

        elif job.getResult() == StartSliceJob.StartJobResult.ObjectSettingError:
            errors = {}
            for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
                stack = node.callDecoration("getStack")
                if not stack:
                    continue
                for key in stack.getErrorKeys():
                    definition = self._global_container_stack.getBottom().findDefinitions(key = key)
                    if not definition:
                        Logger.log("e", "When checking settings for errors, unable to find definition for key {key} in per-object stack.".format(key = key))
                        continue
                    definition = definition[0]
                    errors[key] = definition.label
            error_labels = ", ".join(errors.values())
            self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = error_labels),
                                          title = catalog.i18nc("@info:title", "Unable to slice"))
            self._error_message.show()
            self.backendStateChange.emit(BackendState.Error)
            return

        if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
            if Application.getInstance().platformActivity:
                self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
                                              title = catalog.i18nc("@info:title", "Unable to slice"))
                self._error_message.show()
                self.backendStateChange.emit(BackendState.Error)
            else:
                self.backendStateChange.emit(BackendState.NotStarted)

        if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice:
            if Application.getInstance().platformActivity:
                self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),
                                              title = catalog.i18nc("@info:title", "Unable to slice"))
                self._error_message.show()
                self.backendStateChange.emit(BackendState.Error)
            else:
                self.backendStateChange.emit(BackendState.NotStarted)
            return
        # Preparation completed, send it to the backend.
        self._socket.sendMessage(job.getSliceMessage())

        # Notify the user that it's now up to the backend to do it's job
        self.backendStateChange.emit(BackendState.Processing)

        Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )

    ##  Determine enable or disable auto slicing. Return True for enable timer and False otherwise.
    #   It disables when
    #   - preference auto slice is off
    #   - decorator isBlockSlicing is found (used in g-code reader)
    def determineAutoSlicing(self):
        enable_timer = True

        if not Preferences.getInstance().getValue("general/auto_slice"):
            enable_timer = False
        for node in DepthFirstIterator(self._scene.getRoot()):
            if node.callDecoration("isBlockSlicing"):
                enable_timer = False
                self.backendStateChange.emit(BackendState.Disabled)
            gcode_list = node.callDecoration("getGCodeList")
            if gcode_list is not None:
                self._scene.gcode_list = gcode_list

        if self._use_timer == enable_timer:
            return self._use_timer
        if enable_timer:
            self.backendStateChange.emit(BackendState.NotStarted)
            self.enableTimer()
            return True
        else:
            self.disableTimer()
            return False

    ##  Listener for when the scene has changed.
    #
    #   This should start a slice if the scene is now ready to slice.
    #
    #   \param source The scene node that was changed.
    def _onSceneChanged(self, source):
        if type(source) is not SceneNode:
            return

        root_scene_nodes_changed = False
        if source == self._scene.getRoot():
            num_objects = 0
            for node in DepthFirstIterator(self._scene.getRoot()):
                # Only count sliceable objects
                if node.callDecoration("isSliceable"):
                    num_objects += 1
            if num_objects != self._last_num_objects:
                self._last_num_objects = num_objects
                root_scene_nodes_changed = True
            else:
                return

        if not source.callDecoration("isGroup") and not root_scene_nodes_changed:
            if source.getMeshData() is None:
                return
            if source.getMeshData().getVertices() is None:
                return

        if self._tool_active:
            # do it later, each source only has to be done once
            if source not in self._postponed_scene_change_sources:
                self._postponed_scene_change_sources.append(source)
            return

        self.needsSlicing()
        self.stopSlicing()
        self._onChanged()

    ##  Called when an error occurs in the socket connection towards the engine.
    #
    #   \param error The exception that occurred.
    def _onSocketError(self, error):
        if Application.getInstance().isShuttingDown():
            return

        super()._onSocketError(error)
        if error.getErrorCode() == Arcus.ErrorCode.Debug:
            return

        self._terminate()
        self._createSocket()

        if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
            Logger.log("w", "A socket error caused the connection to be reset")

    ##  Remove old layer data (if any)
    def _clearLayerData(self):
        for node in DepthFirstIterator(self._scene.getRoot()):
            if node.callDecoration("getLayerData"):
                node.getParent().removeChild(node)
                break

    ##  Convenient function: set need_slicing, emit state and clear layer data
    def needsSlicing(self):
        self.stopSlicing()
        self._need_slicing = True
        self.processingProgress.emit(0.0)
        self.backendStateChange.emit(BackendState.NotStarted)
        if not self._use_timer:
            # With manually having to slice, we want to clear the old invalid layer data.
            self._clearLayerData()

    ##  A setting has changed, so check if we must reslice.
    # \param instance The setting instance that has changed.
    # \param property The property of the setting instance that has changed.
    def _onSettingChanged(self, instance, property):
        if property == "value":  # Only reslice if the value has changed.
            self.needsSlicing()
            self._onChanged()

        elif property == "validationState":
            if self._use_timer:
                self._is_error_check_scheduled = True
                self._change_timer.stop()

    def _onStackErrorCheckFinished(self):
        self._is_error_check_scheduled = False
        if not self._slicing and self._need_slicing:
            self.needsSlicing()
            self._onChanged()

    ##  Called when a sliced layer data message is received from the engine.
    #
    #   \param message The protobuf message containing sliced layer data.
    def _onLayerMessage(self, message):
        self._stored_layer_data.append(message)

    ##  Called when an optimized sliced layer data message is received from the engine.
    #
    #   \param message The protobuf message containing sliced layer data.
    def _onOptimizedLayerMessage(self, message):
        self._stored_optimized_layer_data.append(message)

    ##  Called when a progress message is received from the engine.
    #
    #   \param message The protobuf message containing the slicing progress.
    def _onProgressMessage(self, message):
        self.processingProgress.emit(message.amount)
        self.backendStateChange.emit(BackendState.Processing)

    ##  Called when the engine sends a message that slicing is finished.
    #
    #   \param message The protobuf message signalling that slicing is finished.
    def _onSlicingFinishedMessage(self, message):
        self.backendStateChange.emit(BackendState.Done)
        self.processingProgress.emit(1.0)

        for line in self._scene.gcode_list:
            replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
            replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths))
            replaced = replaced.replace("{filament_weight}", str(Application.getInstance().getPrintInformation().materialWeights))
            replaced = replaced.replace("{filament_cost}", str(Application.getInstance().getPrintInformation().materialCosts))
            replaced = replaced.replace("{jobname}", str(Application.getInstance().getPrintInformation().jobName))

            self._scene.gcode_list[self._scene.gcode_list.index(line)] = replaced

        self._slicing = False
        self._need_slicing = False
        Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
        if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()):
            self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
            self._process_layers_job.finished.connect(self._onProcessLayersFinished)
            self._process_layers_job.start()
            self._stored_optimized_layer_data = []

    ##  Called when a g-code message is received from the engine.
    #
    #   \param message The protobuf message containing g-code, encoded as UTF-8.
    def _onGCodeLayerMessage(self, message):
        self._scene.gcode_list.append(message.data.decode("utf-8", "replace"))

    ##  Called when a g-code prefix message is received from the engine.
    #
    #   \param message The protobuf message containing the g-code prefix,
    #   encoded as UTF-8.
    def _onGCodePrefixMessage(self, message):
        self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace"))

    ##  Creates a new socket connection.
    def _createSocket(self):
        super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")))
        self._engine_is_fresh = True

    ##  Called when anything has changed to the stuff that needs to be sliced.
    #
    #   This indicates that we should probably re-slice soon.
    def _onChanged(self, *args, **kwargs):
        self.needsSlicing()
        if self._use_timer:
            # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
            # otherwise business as usual
            if self._is_error_check_scheduled:
                self._change_timer.stop()
            else:
                self._change_timer.start()

    ##  Called when a print time message is received from the engine.
    #
    #   \param message The protobuf message containing the print time per feature and
    #   material amount per extruder
    def _onPrintTimeMaterialEstimates(self, message):
        material_amounts = []
        for index in range(message.repeatedMessageCount("materialEstimates")):
            material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)

        times = self._parseMessagePrintTimes(message)
        self.printDurationMessage.emit(times, material_amounts)

    ##  Called for parsing message to retrieve estimated time per feature
    #
    #   \param message The protobuf message containing the print time per feature
    def _parseMessagePrintTimes(self, message):
        result = {
            "inset_0": message.time_inset_0,
            "inset_x": message.time_inset_x,
            "skin": message.time_skin,
            "infill": message.time_infill,
            "support_infill": message.time_support_infill,
            "support_interface": message.time_support_interface,
            "support": message.time_support,
            "skirt": message.time_skirt,
            "travel": message.time_travel,
            "retract": message.time_retract,
            "none": message.time_none
        }
        return result

    ##  Called when the back-end connects to the front-end.
    def _onBackendConnected(self):
        if self._restart:
            self._restart = False
            self._onChanged()

    ##  Called when the user starts using some tool.
    #
    #   When the user starts using a tool, we should pause slicing to prevent
    #   continuously slicing while the user is dragging some tool handle.
    #
    #   \param tool The tool that the user is using.
    def _onToolOperationStarted(self, tool):
        self._tool_active = True  # Do not react on scene change
        self.disableTimer()
        # Restart engine as soon as possible, we know we want to slice afterwards
        if not self._engine_is_fresh:
            self._terminate()
            self._createSocket()

    ##  Called when the user stops using some tool.
    #
    #   This indicates that we can safely start slicing again.
    #
    #   \param tool The tool that the user was using.
    def _onToolOperationStopped(self, tool):
        self._tool_active = False  # React on scene change again
        self.determineAutoSlicing()  # Switch timer on if appropriate
        # Process all the postponed scene changes
        while self._postponed_scene_change_sources:
            source = self._postponed_scene_change_sources.pop(0)
            self._onSceneChanged(source)

    ##  Called when the user changes the active view mode.
    def _onActiveViewChanged(self):
        if Application.getInstance().getController().getActiveView():
            view = Application.getInstance().getController().getActiveView()
            if view.getPluginId() == "SimulationView":  # If switching to layer view, we should process the layers if that hasn't been done yet.
                self._layer_view_active = True
                # There is data and we're not slicing at the moment
                # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
                if self._stored_optimized_layer_data and not self._slicing:
                    self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
                    self._process_layers_job.finished.connect(self._onProcessLayersFinished)
                    self._process_layers_job.start()
                    self._stored_optimized_layer_data = []
            else:
                self._layer_view_active = False

    ##  Called when the back-end self-terminates.
    #
    #   We should reset our state and start listening for new connections.
    def _onBackendQuit(self):
        if not self._restart:
            if self._process:
                Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
                self._process = None

    ##  Called when the global container stack changes
    def _onGlobalStackChanged(self):
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
            self._global_container_stack.containersChanged.disconnect(self._onChanged)
            extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))

            for extruder in extruders:
                extruder.propertyChanged.disconnect(self._onSettingChanged)
                extruder.containersChanged.disconnect(self._onChanged)

        self._global_container_stack = Application.getInstance().getGlobalContainerStack()

        if self._global_container_stack:
            self._global_container_stack.propertyChanged.connect(self._onSettingChanged)  # Note: Only starts slicing when the value changed.
            self._global_container_stack.containersChanged.connect(self._onChanged)
            extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
            for extruder in extruders:
                extruder.propertyChanged.connect(self._onSettingChanged)
                extruder.containersChanged.connect(self._onChanged)
            self._onChanged()

    def _onProcessLayersFinished(self, job):
        self._process_layers_job = None

    ##  Connect slice function to timer.
    def enableTimer(self):
        if not self._use_timer:
            self._change_timer.timeout.connect(self.slice)
            self._use_timer = True

    ##  Disconnect slice function from timer.
    #   This means that slicing will not be triggered automatically
    def disableTimer(self):
        if self._use_timer:
            self._use_timer = False
            self._change_timer.timeout.disconnect(self.slice)

    def _onPreferencesChanged(self, preference):
        if preference != "general/auto_slice":
            return
        auto_slice = self.determineAutoSlicing()
        if auto_slice:
            self._change_timer.start()

    ##   Tickle the backend so in case of auto slicing, it starts the timer.
    def tickle(self):
        if self._use_timer:
            self._change_timer.start()
Exemple #38
0
    def read(self, file_name):
        Logger.log("d", "Preparing to load %s" % file_name)
        self._cancelled = False

        scene_node = SceneNode()
        # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
        # real data to calculate it from.
        scene_node.getBoundingBox = self._getNullBoundingBox

        gcode_list = []
        self._is_layers_in_file = False

        Logger.log("d", "Opening file %s" % file_name)

        self._extruder_offsets = self._extruderOffsets(
        )  # dict with index the extruder number. can be empty

        last_z = 0
        with open(file_name, "r") as file:
            file_lines = 0
            current_line = 0
            for line in file:
                file_lines += 1
                gcode_list.append(line)
                if not self._is_layers_in_file and line[:len(
                        self._layer_keyword)] == self._layer_keyword:
                    self._is_layers_in_file = True
            file.seek(0)

            file_step = max(math.floor(file_lines / 100), 1)

            self._clearValues()

            self._message = Message(catalog.i18nc("@info:status",
                                                  "Parsing G-code"),
                                    lifetime=0,
                                    title=catalog.i18nc(
                                        "@info:title", "G-code Details"))

            self._message.setProgress(0)
            self._message.show()

            Logger.log("d", "Parsing %s..." % file_name)

            current_position = self._position(0, 0, 0, [0])
            current_path = []

            for line in file:
                if self._cancelled:
                    Logger.log("d", "Parsing %s cancelled" % file_name)
                    return None
                current_line += 1
                last_z = current_position.z

                if current_line % file_step == 0:
                    self._message.setProgress(
                        math.floor(current_line / file_lines * 100))
                    Job.yieldThread()
                if len(line) == 0:
                    continue

                if line.find(self._type_keyword) == 0:
                    type = line[len(self._type_keyword):].strip()
                    if type == "WALL-INNER":
                        self._layer_type = LayerPolygon.InsetXType
                    elif type == "WALL-OUTER":
                        self._layer_type = LayerPolygon.Inset0Type
                    elif type == "SKIN":
                        self._layer_type = LayerPolygon.SkinType
                    elif type == "SKIRT":
                        self._layer_type = LayerPolygon.SkirtType
                    elif type == "SUPPORT":
                        self._layer_type = LayerPolygon.SupportType
                    elif type == "FILL":
                        self._layer_type = LayerPolygon.InfillType
                    else:
                        Logger.log(
                            "w",
                            "Encountered a unknown type (%s) while parsing g-code.",
                            type)

                if self._is_layers_in_file and line[:len(
                        self._layer_keyword)] == self._layer_keyword:
                    try:
                        layer_number = int(line[len(self._layer_keyword):])
                        self._createPolygon(
                            self._current_layer_thickness, current_path,
                            self._extruder_offsets.get(self._extruder_number,
                                                       [0, 0]))
                        current_path.clear()
                        self._layer_number = layer_number
                    except:
                        pass

                # This line is a comment. Ignore it (except for the layer_keyword)
                if line.startswith(";"):
                    continue

                G = self._getInt(line, "G")
                if G is not None:
                    current_position = self._processGCode(
                        G, line, current_position, current_path)

                    # < 2 is a heuristic for a movement only, that should not be counted as a layer
                    if current_position.z > last_z and abs(current_position.z -
                                                           last_z) < 2:
                        if self._createPolygon(
                                self._current_layer_thickness, current_path,
                                self._extruder_offsets.get(
                                    self._extruder_number, [0, 0])):
                            current_path.clear()
                            if not self._is_layers_in_file:
                                self._layer_number += 1

                    continue

                if line.startswith("T"):
                    T = self._getInt(line, "T")
                    if T is not None:
                        self._createPolygon(
                            self._current_layer_thickness, current_path,
                            self._extruder_offsets.get(self._extruder_number,
                                                       [0, 0]))
                        current_path.clear()

                        current_position = self._processTCode(
                            T, line, current_position, current_path)

            # "Flush" leftovers
            if not self._is_layers_in_file and len(current_path) > 1:
                if self._createPolygon(
                        self._current_layer_thickness, current_path,
                        self._extruder_offsets.get(self._extruder_number,
                                                   [0, 0])):
                    self._layer_number += 1
                    current_path.clear()

        material_color_map = numpy.zeros((10, 4), dtype=numpy.float32)
        material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
        material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
        layer_mesh = self._layer_data_builder.build(material_color_map)
        decorator = LayerDataDecorator.LayerDataDecorator()
        decorator.setLayerData(layer_mesh)
        scene_node.addDecorator(decorator)

        gcode_list_decorator = GCodeListDecorator()
        gcode_list_decorator.setGCodeList(gcode_list)
        scene_node.addDecorator(gcode_list_decorator)

        Application.getInstance().getController().getScene(
        ).gcode_list = gcode_list

        Logger.log("d", "Finished parsing %s" % file_name)
        self._message.hide()

        if self._layer_number == 0:
            Logger.log("w",
                       "File %s doesn't contain any valid layers" % file_name)

        settings = Application.getInstance().getGlobalContainerStack()
        machine_width = settings.getProperty("machine_width", "value")
        machine_depth = settings.getProperty("machine_depth", "value")

        if not self._center_is_zero:
            scene_node.setPosition(
                Vector(-machine_width / 2, 0, machine_depth / 2))

        Logger.log("d", "Loaded %s" % file_name)

        if Preferences.getInstance().getValue("gcodereader/show_caution"):
            caution_message = Message(catalog.i18nc(
                "@info:generic",
                "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."
            ),
                                      lifetime=0,
                                      title=catalog.i18nc(
                                          "@info:title", "G-code Details"))
            caution_message.show()

        # The "save/print" button's state is bound to the backend state.
        backend = Application.getInstance().getBackend()
        backend.backendStateChange.emit(Backend.BackendState.Disabled)

        return scene_node
Exemple #39
0
class GCodeReader(MeshReader):
    def __init__(self):
        super(GCodeReader, self).__init__()
        self._supported_extensions = [".gcode", ".g"]
        Application.getInstance().hideMessageSignal.connect(
            self._onHideMessage)
        self._cancelled = False
        self._message = None
        self._layer_number = 0
        self._extruder_number = 0
        self._clearValues()
        self._scene_node = None
        self._position = namedtuple('Position', ['x', 'y', 'z', 'e'])
        self._is_layers_in_file = False  # Does the Gcode have the layers comment?
        self._extruder_offsets = {
        }  # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
        self._current_layer_thickness = 0.2  # default

        Preferences.getInstance().addPreference("gcodereader/show_caution",
                                                True)

    def _clearValues(self):
        self._extruder_number = 0
        self._layer_type = LayerPolygon.Inset0Type
        self._layer_number = 0
        self._previous_z = 0
        self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
        self._center_is_zero = False

    @staticmethod
    def _getValue(line, code):
        n = line.find(code)
        if n < 0:
            return None
        n += len(code)
        pattern = re.compile("[;\s]")
        match = pattern.search(line, n)
        m = match.start() if match is not None else -1
        try:
            if m < 0:
                return line[n:]
            return line[n:m]
        except:
            return None

    def _getInt(self, line, code):
        value = self._getValue(line, code)
        try:
            return int(value)
        except:
            return None

    def _getFloat(self, line, code):
        value = self._getValue(line, code)
        try:
            return float(value)
        except:
            return None

    def _onHideMessage(self, message):
        if message == self._message:
            self._cancelled = True

    @staticmethod
    def _getNullBoundingBox():
        return AxisAlignedBox(minimum=Vector(0, 0, 0),
                              maximum=Vector(10, 10, 10))

    def _createPolygon(self, layer_thickness, path, extruder_offsets):
        countvalid = 0
        for point in path:
            if point[3] > 0:
                countvalid += 1
                if countvalid >= 2:
                    # we know what to do now, no need to count further
                    continue
        if countvalid < 2:
            return False
        try:
            self._layer_data_builder.addLayer(self._layer_number)
            self._layer_data_builder.setLayerHeight(self._layer_number,
                                                    path[0][2])
            self._layer_data_builder.setLayerThickness(self._layer_number,
                                                       layer_thickness)
            this_layer = self._layer_data_builder.getLayer(self._layer_number)
        except ValueError:
            return False
        count = len(path)
        line_types = numpy.empty((count - 1, 1), numpy.int32)
        line_widths = numpy.empty((count - 1, 1), numpy.float32)
        line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
        # TODO: need to calculate actual line width based on E values
        line_widths[:, 0] = 0.35  # Just a guess
        line_thicknesses[:, 0] = layer_thickness
        points = numpy.empty((count, 3), numpy.float32)
        i = 0
        for point in path:
            points[i, :] = [
                point[0] + extruder_offsets[0], point[2],
                -point[1] - extruder_offsets[1]
            ]
            if i > 0:
                line_types[i - 1] = point[3]
                if point[3] in [
                        LayerPolygon.MoveCombingType,
                        LayerPolygon.MoveRetractionType
                ]:
                    line_widths[i - 1] = 0.1
            i += 1

        this_poly = LayerPolygon(self._extruder_number, line_types, points,
                                 line_widths, line_thicknesses)
        this_poly.buildCache()

        this_layer.polygons.append(this_poly)
        return True

    def _gCode0(self, position, params, path):
        x, y, z, e = position
        x = params.x if params.x is not None else x
        y = params.y if params.y is not None else y
        z = params.z if params.z is not None else position.z

        if params.e is not None:
            if params.e > e[self._extruder_number]:
                path.append([x, y, z, self._layer_type])  # extrusion
            else:
                path.append([x, y, z,
                             LayerPolygon.MoveRetractionType])  # retraction
            e[self._extruder_number] = params.e

            # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
            # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
            if z > self._previous_z and (z - self._previous_z < 1.5):
                self._current_layer_thickness = z - self._previous_z + 0.05  # allow a tiny overlap
                self._previous_z = z
        else:
            path.append([x, y, z, LayerPolygon.MoveCombingType])
        return self._position(x, y, z, e)

    # G0 and G1 should be handled exactly the same.
    _gCode1 = _gCode0

    ##  Home the head.
    def _gCode28(self, position, params, path):
        return self._position(params.x if params.x is not None else position.x,
                              params.y if params.y is not None else position.y,
                              0, position.e)

    ##  Reset the current position to the values specified.
    #   For example: G92 X10 will set the X to 10 without any physical motion.
    def _gCode92(self, position, params, path):
        if params.e is not None:
            position.e[self._extruder_number] = params.e
        return self._position(params.x if params.x is not None else position.x,
                              params.y if params.y is not None else position.y,
                              params.z if params.z is not None else position.z,
                              position.e)

    def _processGCode(self, G, line, position, path):
        func = getattr(self, "_gCode%s" % G, None)
        line = line.split(";", 1)[0]  # Remove comments (if any)
        if func is not None:
            s = line.upper().split(" ")
            x, y, z, e = None, None, None, None
            for item in s[1:]:
                if len(item) <= 1:
                    continue
                if item.startswith(";"):
                    continue
                if item[0] == "X":
                    x = float(item[1:])
                if item[0] == "Y":
                    y = float(item[1:])
                if item[0] == "Z":
                    z = float(item[1:])
                if item[0] == "E":
                    e = float(item[1:])
            if (x is not None and x < 0) or (y is not None and y < 0):
                self._center_is_zero = True
            params = self._position(x, y, z, e)
            return func(position, params, path)
        return position

    def _processTCode(self, T, line, position, path):
        self._extruder_number = T
        if self._extruder_number + 1 > len(position.e):
            position.e.extend([0] *
                              (self._extruder_number - len(position.e) + 1))
        return position

    _type_keyword = ";TYPE:"
    _layer_keyword = ";LAYER:"

    ##  For showing correct x, y offsets for each extruder
    def _extruderOffsets(self):
        result = {}
        for extruder in ExtruderManager.getInstance().getExtruderStacks():
            result[int(extruder.getMetaData().get("position", "0"))] = [
                extruder.getProperty("machine_nozzle_offset_x", "value"),
                extruder.getProperty("machine_nozzle_offset_y", "value")
            ]
        return result

    def read(self, file_name):
        Logger.log("d", "Preparing to load %s" % file_name)
        self._cancelled = False

        scene_node = SceneNode()
        # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
        # real data to calculate it from.
        scene_node.getBoundingBox = self._getNullBoundingBox

        gcode_list = []
        self._is_layers_in_file = False

        Logger.log("d", "Opening file %s" % file_name)

        self._extruder_offsets = self._extruderOffsets(
        )  # dict with index the extruder number. can be empty

        last_z = 0
        with open(file_name, "r") as file:
            file_lines = 0
            current_line = 0
            for line in file:
                file_lines += 1
                gcode_list.append(line)
                if not self._is_layers_in_file and line[:len(
                        self._layer_keyword)] == self._layer_keyword:
                    self._is_layers_in_file = True
            file.seek(0)

            file_step = max(math.floor(file_lines / 100), 1)

            self._clearValues()

            self._message = Message(catalog.i18nc("@info:status",
                                                  "Parsing G-code"),
                                    lifetime=0,
                                    title=catalog.i18nc(
                                        "@info:title", "G-code Details"))

            self._message.setProgress(0)
            self._message.show()

            Logger.log("d", "Parsing %s..." % file_name)

            current_position = self._position(0, 0, 0, [0])
            current_path = []

            for line in file:
                if self._cancelled:
                    Logger.log("d", "Parsing %s cancelled" % file_name)
                    return None
                current_line += 1
                last_z = current_position.z

                if current_line % file_step == 0:
                    self._message.setProgress(
                        math.floor(current_line / file_lines * 100))
                    Job.yieldThread()
                if len(line) == 0:
                    continue

                if line.find(self._type_keyword) == 0:
                    type = line[len(self._type_keyword):].strip()
                    if type == "WALL-INNER":
                        self._layer_type = LayerPolygon.InsetXType
                    elif type == "WALL-OUTER":
                        self._layer_type = LayerPolygon.Inset0Type
                    elif type == "SKIN":
                        self._layer_type = LayerPolygon.SkinType
                    elif type == "SKIRT":
                        self._layer_type = LayerPolygon.SkirtType
                    elif type == "SUPPORT":
                        self._layer_type = LayerPolygon.SupportType
                    elif type == "FILL":
                        self._layer_type = LayerPolygon.InfillType
                    else:
                        Logger.log(
                            "w",
                            "Encountered a unknown type (%s) while parsing g-code.",
                            type)

                if self._is_layers_in_file and line[:len(
                        self._layer_keyword)] == self._layer_keyword:
                    try:
                        layer_number = int(line[len(self._layer_keyword):])
                        self._createPolygon(
                            self._current_layer_thickness, current_path,
                            self._extruder_offsets.get(self._extruder_number,
                                                       [0, 0]))
                        current_path.clear()
                        self._layer_number = layer_number
                    except:
                        pass

                # This line is a comment. Ignore it (except for the layer_keyword)
                if line.startswith(";"):
                    continue

                G = self._getInt(line, "G")
                if G is not None:
                    current_position = self._processGCode(
                        G, line, current_position, current_path)

                    # < 2 is a heuristic for a movement only, that should not be counted as a layer
                    if current_position.z > last_z and abs(current_position.z -
                                                           last_z) < 2:
                        if self._createPolygon(
                                self._current_layer_thickness, current_path,
                                self._extruder_offsets.get(
                                    self._extruder_number, [0, 0])):
                            current_path.clear()
                            if not self._is_layers_in_file:
                                self._layer_number += 1

                    continue

                if line.startswith("T"):
                    T = self._getInt(line, "T")
                    if T is not None:
                        self._createPolygon(
                            self._current_layer_thickness, current_path,
                            self._extruder_offsets.get(self._extruder_number,
                                                       [0, 0]))
                        current_path.clear()

                        current_position = self._processTCode(
                            T, line, current_position, current_path)

            # "Flush" leftovers
            if not self._is_layers_in_file and len(current_path) > 1:
                if self._createPolygon(
                        self._current_layer_thickness, current_path,
                        self._extruder_offsets.get(self._extruder_number,
                                                   [0, 0])):
                    self._layer_number += 1
                    current_path.clear()

        material_color_map = numpy.zeros((10, 4), dtype=numpy.float32)
        material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
        material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
        layer_mesh = self._layer_data_builder.build(material_color_map)
        decorator = LayerDataDecorator.LayerDataDecorator()
        decorator.setLayerData(layer_mesh)
        scene_node.addDecorator(decorator)

        gcode_list_decorator = GCodeListDecorator()
        gcode_list_decorator.setGCodeList(gcode_list)
        scene_node.addDecorator(gcode_list_decorator)

        Application.getInstance().getController().getScene(
        ).gcode_list = gcode_list

        Logger.log("d", "Finished parsing %s" % file_name)
        self._message.hide()

        if self._layer_number == 0:
            Logger.log("w",
                       "File %s doesn't contain any valid layers" % file_name)

        settings = Application.getInstance().getGlobalContainerStack()
        machine_width = settings.getProperty("machine_width", "value")
        machine_depth = settings.getProperty("machine_depth", "value")

        if not self._center_is_zero:
            scene_node.setPosition(
                Vector(-machine_width / 2, 0, machine_depth / 2))

        Logger.log("d", "Loaded %s" % file_name)

        if Preferences.getInstance().getValue("gcodereader/show_caution"):
            caution_message = Message(catalog.i18nc(
                "@info:generic",
                "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."
            ),
                                      lifetime=0,
                                      title=catalog.i18nc(
                                          "@info:title", "G-code Details"))
            caution_message.show()

        # The "save/print" button's state is bound to the backend state.
        backend = Application.getInstance().getBackend()
        backend.backendStateChange.emit(Backend.BackendState.Disabled)

        return scene_node
class CuraEngineBackend(QObject, Backend):
    backendError = Signal()

    ##  Starts the back-end plug-in.
    #
    #   This registers all the signal listeners and prepares for communication
    #   with the back-end in general.
    #   CuraEngineBackend is exposed to qml as well.
    def __init__(self) -> None:
        super().__init__()
        # Find out where the engine is located, and how it is called.
        # This depends on how Cura is packaged and which OS we are running on.
        executable_name = "CuraEngine"
        if Platform.isWindows():
            executable_name += ".exe"
        default_engine_location = executable_name
        if os.path.exists(
                os.path.join(CuraApplication.getInstallPrefix(), "bin",
                             executable_name)):
            default_engine_location = os.path.join(
                CuraApplication.getInstallPrefix(), "bin", executable_name)
        if hasattr(sys, "frozen"):
            default_engine_location = os.path.join(
                os.path.dirname(os.path.abspath(sys.executable)),
                executable_name)
        if Platform.isLinux() and not default_engine_location:
            if not os.getenv("PATH"):
                raise OSError(
                    "There is something wrong with your Linux installation.")
            for pathdir in cast(str, os.getenv("PATH")).split(os.pathsep):
                execpath = os.path.join(pathdir, executable_name)
                if os.path.exists(execpath):
                    default_engine_location = execpath
                    break

        self._application = CuraApplication.getInstance(
        )  #type: CuraApplication
        self._multi_build_plate_model = None  #type: Optional[MultiBuildPlateModel]
        self._machine_error_checker = None  #type: Optional[MachineErrorChecker]

        if not default_engine_location:
            raise EnvironmentError("Could not find CuraEngine")

        Logger.log("i", "Found CuraEngine at: %s", default_engine_location)

        default_engine_location = os.path.abspath(default_engine_location)
        self._application.getPreferences().addPreference(
            "backend/location", default_engine_location)

        # Workaround to disable layer view processing if layer view is not active.
        self._layer_view_active = False  #type: bool
        self._onActiveViewChanged()

        self._stored_layer_data = []  # type: List[Arcus.PythonMessage]
        self._stored_optimized_layer_data = {
        }  # type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob

        self._scene = self._application.getController().getScene(
        )  #type: Scene
        self._scene.sceneChanged.connect(self._onSceneChanged)

        # Triggers for auto-slicing. Auto-slicing is triggered as follows:
        #  - auto-slicing is started with a timer
        #  - whenever there is a value change, we start the timer
        #  - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the
        #    auto-slicing timer when that error check is finished
        # If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished
        # to start the auto-slicing timer again.
        #
        self._global_container_stack = None  #type: Optional[ContainerStack]

        # Listeners for receiving messages from the back-end.
        self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
        self._message_handlers[
            "cura.proto.LayerOptimized"] = self._onOptimizedLayerMessage
        self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
        self._message_handlers[
            "cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
        self._message_handlers[
            "cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
        self._message_handlers[
            "cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
        self._message_handlers[
            "cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage

        self._start_slice_job = None  #type: Optional[StartSliceJob]
        self._start_slice_job_build_plate = None  #type: Optional[int]
        self._slicing = False  #type: bool # Are we currently slicing?
        self._restart = False  #type: bool # Back-end is currently restarting?
        self._tool_active = False  #type: bool # If a tool is active, some tasks do not have to do anything
        self._always_restart = True  #type: bool # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
        self._process_layers_job = None  #type: Optional[ProcessSlicedLayersJob] # The currently active job to process layers, or None if it is not processing layers.
        self._build_plates_to_be_sliced = [
        ]  #type: List[int] # what needs slicing?
        self._engine_is_fresh = True  #type: bool # Is the newly started engine used before or not?

        self._backend_log_max_lines = 20000  #type: int # Maximum number of lines to buffer
        self._error_message = None  #type: Optional[Message] # Pop-up message that shows errors.
        self._last_num_objects = defaultdict(
            int
        )  #type: Dict[int, int] # Count number of objects to see if there is something changed
        self._postponed_scene_change_sources = [
        ]  #type: List[SceneNode] # scene change is postponed (by a tool)

        self._slice_start_time = None  #type: Optional[float]
        self._is_disabled = False  #type: bool

        self._application.getPreferences().addPreference(
            "general/auto_slice", False)

        self._use_timer = False  #type: bool
        # When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
        # This timer will group them up, and only slice for the last setting changed signal.
        # TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
        self._change_timer = QTimer()  #type: QTimer
        self._change_timer.setSingleShot(True)
        self._change_timer.setInterval(500)
        self.determineAutoSlicing()
        self._application.getPreferences().preferenceChanged.connect(
            self._onPreferencesChanged)

        self._application.initializationFinished.connect(self.initialize)

    def initialize(self) -> None:
        self._multi_build_plate_model = self._application.getMultiBuildPlateModel(
        )

        self._application.getController().activeViewChanged.connect(
            self._onActiveViewChanged)

        if self._multi_build_plate_model:
            self._multi_build_plate_model.activeBuildPlateChanged.connect(
                self._onActiveViewChanged)

        self._application.getMachineManager().globalContainerChanged.connect(
            self._onGlobalStackChanged)
        self._onGlobalStackChanged()

        # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash
        ExtruderManager.getInstance().extrudersChanged.connect(
            self._extruderChanged)

        self.backendQuit.connect(self._onBackendQuit)
        self.backendConnected.connect(self._onBackendConnected)

        # When a tool operation is in progress, don't slice. So we need to listen for tool operations.
        self._application.getController().toolOperationStarted.connect(
            self._onToolOperationStarted)
        self._application.getController().toolOperationStopped.connect(
            self._onToolOperationStopped)

        self._machine_error_checker = self._application.getMachineErrorChecker(
        )
        self._machine_error_checker.errorCheckFinished.connect(
            self._onStackErrorCheckFinished)

    ##  Terminate the engine process.
    #
    #   This function should terminate the engine process.
    #   Called when closing the application.
    def close(self) -> None:
        # Terminate CuraEngine if it is still running at this point
        self._terminate()

    ##  Get the command that is used to call the engine.
    #   This is useful for debugging and used to actually start the engine.
    #   \return list of commands and args / parameters.
    def getEngineCommand(self) -> List[str]:
        command = [
            self._application.getPreferences().getValue("backend/location"),
            "connect", "127.0.0.1:{0}".format(self._port), ""
        ]

        parser = argparse.ArgumentParser(prog="cura", add_help=False)
        parser.add_argument(
            "--debug",
            action="store_true",
            default=False,
            help="Turn on the debug mode by setting this option.")
        known_args = vars(parser.parse_known_args()[0])
        if known_args["debug"]:
            command.append("-vvv")

        return command

    ##  Emitted when we get a message containing print duration and material amount.
    #   This also implies the slicing has finished.
    #   \param time The amount of time the print will take.
    #   \param material_amount The amount of material the print will use.
    printDurationMessage = Signal()

    ##  Emitted when the slicing process starts.
    slicingStarted = Signal()

    ##  Emitted when the slicing process is aborted forcefully.
    slicingCancelled = Signal()

    @pyqtSlot()
    def stopSlicing(self) -> None:
        self.setState(BackendState.NotStarted)
        if self._slicing:  # We were already slicing. Stop the old job.
            self._terminate()
            self._createSocket()

        if self._process_layers_job is not None:  # We were processing layers. Stop that, the layers are going to change soon.
            Logger.log("d", "Aborting process layers job...")
            self._process_layers_job.abort()
            self._process_layers_job = None

        if self._error_message:
            self._error_message.hide()

    ##  Manually triggers a reslice
    @pyqtSlot()
    def forceSlice(self) -> None:
        self.markSliceAll()
        self.slice()

    ##  Perform a slice of the scene.
    def slice(self) -> None:
        Logger.log("d", "Starting to slice...")
        self._slice_start_time = time()
        if not self._build_plates_to_be_sliced:
            self.processingProgress.emit(1.0)
            Logger.log(
                "w",
                "Slice unnecessary, nothing has changed that needs reslicing.")
            self.setState(BackendState.Done)
            return

        if self._process_layers_job:
            Logger.log("d", "Process layers job still busy, trying later.")
            return

        if not hasattr(self._scene, "gcode_dict"):
            self._scene.gcode_dict = {
            }  #type: ignore #Because we are creating the missing attribute here.

        # see if we really have to slice
        active_build_plate = self._application.getMultiBuildPlateModel(
        ).activeBuildPlate
        build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
        Logger.log(
            "d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
        num_objects = self._numObjectsPerBuildPlate()

        self._stored_layer_data = []

        if build_plate_to_be_sliced not in num_objects or num_objects[
                build_plate_to_be_sliced] == 0:
            self._scene.gcode_dict[build_plate_to_be_sliced] = [
            ]  #type: ignore #Because we created this attribute above.
            Logger.log("d",
                       "Build plate %s has no objects to be sliced, skipping",
                       build_plate_to_be_sliced)
            if self._build_plates_to_be_sliced:
                self.slice()
            return
        self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
        if self._application.getPrintInformation(
        ) and build_plate_to_be_sliced == active_build_plate:
            self._application.getPrintInformation().setToZeroPrintInformation(
                build_plate_to_be_sliced)

        if self._process is None:  # type: ignore
            self._createSocket()
        self.stopSlicing()
        self._engine_is_fresh = False  # Yes we're going to use the engine

        self.processingProgress.emit(0.0)
        self.backendStateChange.emit(BackendState.NotStarted)

        self._scene.gcode_dict[build_plate_to_be_sliced] = [
        ]  #type: ignore #[] indexed by build plate number
        self._slicing = True
        self.slicingStarted.emit()

        self.determineAutoSlicing()  # Switch timer on or off if appropriate

        slice_message = self._socket.createMessage("cura.proto.Slice")
        self._start_slice_job = StartSliceJob(slice_message)
        self._start_slice_job_build_plate = build_plate_to_be_sliced
        self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate)
        self._start_slice_job.start()
        self._start_slice_job.finished.connect(self._onStartSliceCompleted)

    ##  Terminate the engine process.
    #   Start the engine process by calling _createSocket()
    def _terminate(self) -> None:
        self._slicing = False
        self._stored_layer_data = []
        if self._start_slice_job_build_plate in self._stored_optimized_layer_data:
            del self._stored_optimized_layer_data[
                self._start_slice_job_build_plate]
        if self._start_slice_job is not None:
            self._start_slice_job.cancel()

        self.slicingCancelled.emit()
        self.processingProgress.emit(0)
        Logger.log("d", "Attempting to kill the engine process")

        if self._application.getUseExternalBackend():
            return

        if self._process is not None:  # type: ignore
            Logger.log("d", "Killing engine process")
            try:
                self._process.terminate()  # type: ignore
                Logger.log("d",
                           "Engine process is killed. Received return code %s",
                           self._process.wait())  # type: ignore
                self._process = None  # type: ignore

            except Exception as e:  # terminating a process that is already terminating causes an exception, silently ignore this.
                Logger.log(
                    "d",
                    "Exception occurred while trying to kill the engine %s",
                    str(e))

    ##  Event handler to call when the job to initiate the slicing process is
    #   completed.
    #
    #   When the start slice job is successfully completed, it will be happily
    #   slicing. This function handles any errors that may occur during the
    #   bootstrapping of a slice job.
    #
    #   \param job The start slice job that was just finished.
    def _onStartSliceCompleted(self, job: StartSliceJob) -> None:
        if self._error_message:
            self._error_message.hide()

        # Note that cancelled slice jobs can still call this method.
        if self._start_slice_job is job:
            self._start_slice_job = None

        if job.isCancelled() or job.getError() or job.getResult(
        ) == StartJobResult.Error:
            self.setState(BackendState.Error)
            self.backendError.emit(job)
            return

        if job.getResult() == StartJobResult.MaterialIncompatible:
            if self._application.platformActivity:
                self._error_message = Message(catalog.i18nc(
                    "@info:status",
                    "Unable to slice with the current material as it is incompatible with the selected machine or configuration."
                ),
                                              title=catalog.i18nc(
                                                  "@info:title",
                                                  "Unable to slice"))
                self._error_message.show()
                self.setState(BackendState.Error)
                self.backendError.emit(job)
            else:
                self.setState(BackendState.NotStarted)
            return

        if job.getResult() == StartJobResult.SettingError:
            if self._application.platformActivity:
                if not self._global_container_stack:
                    Logger.log(
                        "w",
                        "Global container stack not assigned to CuraEngineBackend!"
                    )
                    return
                extruders = ExtruderManager.getInstance(
                ).getActiveExtruderStacks()
                error_keys = []  #type: List[str]
                for extruder in extruders:
                    error_keys.extend(extruder.getErrorKeys())
                if not extruders:
                    error_keys = self._global_container_stack.getErrorKeys()
                error_labels = set()
                for key in error_keys:
                    for stack in [
                            self._global_container_stack
                    ] + extruders:  #Search all container stacks for the definition of this setting. Some are only in an extruder stack.
                        definitions = cast(
                            DefinitionContainerInterface,
                            stack.getBottom()).findDefinitions(key=key)
                        if definitions:
                            break  #Found it! No need to continue search.
                    else:  #No stack has a definition for this setting.
                        Logger.log(
                            "w",
                            "When checking settings for errors, unable to find definition for key: {key}"
                            .format(key=key))
                        continue
                    error_labels.add(definitions[0].label)

                self._error_message = Message(catalog.i18nc(
                    "@info:status",
                    "Unable to slice with the current settings. The following settings have errors: {0}"
                ).format(", ".join(error_labels)),
                                              title=catalog.i18nc(
                                                  "@info:title",
                                                  "Unable to slice"))
                self._error_message.show()
                self.setState(BackendState.Error)
                self.backendError.emit(job)
            else:
                self.setState(BackendState.NotStarted)
            return

        elif job.getResult() == StartJobResult.ObjectSettingError:
            errors = {}
            for node in DepthFirstIterator(
                    self._application.getController().getScene().getRoot()
            ):  #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
                stack = node.callDecoration("getStack")
                if not stack:
                    continue
                for key in stack.getErrorKeys():
                    if not self._global_container_stack:
                        Logger.log(
                            "e",
                            "CuraEngineBackend does not have global_container_stack assigned."
                        )
                        continue
                    definition = cast(DefinitionContainerInterface,
                                      self._global_container_stack.getBottom()
                                      ).findDefinitions(key=key)
                    if not definition:
                        Logger.log(
                            "e",
                            "When checking settings for errors, unable to find definition for key {key} in per-object stack."
                            .format(key=key))
                        continue
                    errors[key] = definition[0].label
            self._error_message = Message(catalog.i18nc(
                "@info:status",
                "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}"
            ).format(error_labels=", ".join(errors.values())),
                                          title=catalog.i18nc(
                                              "@info:title",
                                              "Unable to slice"))
            self._error_message.show()
            self.setState(BackendState.Error)
            self.backendError.emit(job)
            return

        if job.getResult() == StartJobResult.BuildPlateError:
            if self._application.platformActivity:
                self._error_message = Message(catalog.i18nc(
                    "@info:status",
                    "Unable to slice because the prime tower or prime position(s) are invalid."
                ),
                                              title=catalog.i18nc(
                                                  "@info:title",
                                                  "Unable to slice"))
                self._error_message.show()
                self.setState(BackendState.Error)
                self.backendError.emit(job)
            else:
                self.setState(BackendState.NotStarted)

        if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder:
            self._error_message = Message(catalog.i18nc(
                "@info:status",
                "Unable to slice because there are objects associated with disabled Extruder %s."
                % job.getMessage()),
                                          title=catalog.i18nc(
                                              "@info:title",
                                              "Unable to slice"))
            self._error_message.show()
            self.setState(BackendState.Error)
            self.backendError.emit(job)
            return

        if job.getResult() == StartJobResult.NothingToSlice:
            if self._application.platformActivity:
                self._error_message = Message(catalog.i18nc(
                    "@info:status",
                    "Nothing to slice because none of the models fit the build volume or are assigned to a disabled extruder. Please scale or rotate models to fit, or enable an extruder."
                ),
                                              title=catalog.i18nc(
                                                  "@info:title",
                                                  "Unable to slice"))
                self._error_message.show()
                self.setState(BackendState.Error)
                self.backendError.emit(job)
            else:
                self.setState(BackendState.NotStarted)
            self._invokeSlice()
            return

        # Preparation completed, send it to the backend.
        self._socket.sendMessage(job.getSliceMessage())

        # Notify the user that it's now up to the backend to do it's job
        self.setState(BackendState.Processing)

        if self._slice_start_time:
            Logger.log("d", "Sending slice message took %s seconds",
                       time() - self._slice_start_time)

    ##  Determine enable or disable auto slicing. Return True for enable timer and False otherwise.
    #   It disables when
    #   - preference auto slice is off
    #   - decorator isBlockSlicing is found (used in g-code reader)
    def determineAutoSlicing(self) -> bool:
        enable_timer = True
        self._is_disabled = False

        if not self._application.getPreferences().getValue(
                "general/auto_slice"):
            enable_timer = False
        for node in DepthFirstIterator(
                self._scene.getRoot()
        ):  #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
            if node.callDecoration("isBlockSlicing"):
                enable_timer = False
                self.setState(BackendState.Disabled)
                self._is_disabled = True
            gcode_list = node.callDecoration("getGCodeList")
            if gcode_list is not None:
                self._scene.gcode_dict[node.callDecoration(
                    "getBuildPlateNumber"
                )] = gcode_list  #type: ignore #Because we generate this attribute dynamically.

        if self._use_timer == enable_timer:
            return self._use_timer
        if enable_timer:
            self.setState(BackendState.NotStarted)
            self.enableTimer()
            return True
        else:
            self.disableTimer()
            return False

    ##  Return a dict with number of objects per build plate
    def _numObjectsPerBuildPlate(self) -> Dict[int, int]:
        num_objects = defaultdict(int)  #type: Dict[int, int]
        for node in DepthFirstIterator(
                self._scene.getRoot()
        ):  #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
            # Only count sliceable objects
            if node.callDecoration("isSliceable"):
                build_plate_number = node.callDecoration("getBuildPlateNumber")
                if build_plate_number is not None:
                    num_objects[build_plate_number] += 1
        return num_objects

    ##  Listener for when the scene has changed.
    #
    #   This should start a slice if the scene is now ready to slice.
    #
    #   \param source The scene node that was changed.
    def _onSceneChanged(self, source: SceneNode) -> None:
        if not isinstance(source, SceneNode):
            return

        # This case checks if the source node is a node that contains GCode. In this case the
        # current layer data is removed so the previous data is not rendered - CURA-4821
        if source.callDecoration("isBlockSlicing") and source.callDecoration(
                "getLayerData"):
            self._stored_optimized_layer_data = {}

        build_plate_changed = set()
        source_build_plate_number = source.callDecoration(
            "getBuildPlateNumber")
        if source == self._scene.getRoot():
            # we got the root node
            num_objects = self._numObjectsPerBuildPlate()
            for build_plate_number in list(
                    self._last_num_objects.keys()) + list(num_objects.keys()):
                if build_plate_number not in self._last_num_objects or num_objects[
                        build_plate_number] != self._last_num_objects[
                            build_plate_number]:
                    self._last_num_objects[build_plate_number] = num_objects[
                        build_plate_number]
                    build_plate_changed.add(build_plate_number)
        else:
            # we got a single scenenode
            if not source.callDecoration("isGroup"):
                mesh_data = source.getMeshData()
                if mesh_data is None or mesh_data.getVertices() is None:
                    return

            # There are some SceneNodes that do not have any build plate associated, then do not add to the list.
            if source_build_plate_number is not None:
                build_plate_changed.add(source_build_plate_number)

        if not build_plate_changed:
            return

        if self._tool_active:
            # do it later, each source only has to be done once
            if source not in self._postponed_scene_change_sources:
                self._postponed_scene_change_sources.append(source)
            return

        self.stopSlicing()
        for build_plate_number in build_plate_changed:
            if build_plate_number not in self._build_plates_to_be_sliced:
                self._build_plates_to_be_sliced.append(build_plate_number)
            self.printDurationMessage.emit(source_build_plate_number, {}, [])
        self.processingProgress.emit(0.0)
        self.setState(BackendState.NotStarted)
        # if not self._use_timer:
        # With manually having to slice, we want to clear the old invalid layer data.
        self._clearLayerData(build_plate_changed)

        self._invokeSlice()

    ##  Called when an error occurs in the socket connection towards the engine.
    #
    #   \param error The exception that occurred.
    def _onSocketError(self, error: Arcus.Error) -> None:
        if self._application.isShuttingDown():
            return

        super()._onSocketError(error)
        if error.getErrorCode() == Arcus.ErrorCode.Debug:
            return

        self._terminate()
        self._createSocket()

        if error.getErrorCode() not in [
                Arcus.ErrorCode.BindFailedError,
                Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug
        ]:
            Logger.log("w", "A socket error caused the connection to be reset")

        # _terminate()' function sets the job status to 'cancel', after reconnecting to another Port the job status
        # needs to be updated. Otherwise backendState is "Unable To Slice"
        if error.getErrorCode(
        ) == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None:
            self._start_slice_job.setIsCancelled(False)

    ##  Remove old layer data (if any)
    def _clearLayerData(self, build_plate_numbers: Set = None) -> None:
        # Clear out any old gcode
        self._scene.gcode_dict = {}  # type: ignore

        for node in DepthFirstIterator(
                self._scene.getRoot()
        ):  #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
            if node.callDecoration("getLayerData"):
                if not build_plate_numbers or node.callDecoration(
                        "getBuildPlateNumber") in build_plate_numbers:
                    node.getParent().removeChild(node)

    def markSliceAll(self) -> None:
        for build_plate_number in range(
                self._application.getMultiBuildPlateModel().maxBuildPlate + 1):
            if build_plate_number not in self._build_plates_to_be_sliced:
                self._build_plates_to_be_sliced.append(build_plate_number)

    ##  Convenient function: mark everything to slice, emit state and clear layer data
    def needsSlicing(self) -> None:
        self.stopSlicing()
        self.markSliceAll()
        self.processingProgress.emit(0.0)
        self.setState(BackendState.NotStarted)
        if not self._use_timer:
            # With manually having to slice, we want to clear the old invalid layer data.
            self._clearLayerData()

    ##  A setting has changed, so check if we must reslice.
    # \param instance The setting instance that has changed.
    # \param property The property of the setting instance that has changed.
    def _onSettingChanged(self, instance: SettingInstance,
                          property: str) -> None:
        if property == "value":  # Only reslice if the value has changed.
            self.needsSlicing()
            self._onChanged()

        elif property == "validationState":
            if self._use_timer:
                self._change_timer.stop()

    def _onStackErrorCheckFinished(self) -> None:
        self.determineAutoSlicing()
        if self._is_disabled:
            return

        if not self._slicing and self._build_plates_to_be_sliced:
            self.needsSlicing()
            self._onChanged()

    ##  Called when a sliced layer data message is received from the engine.
    #
    #   \param message The protobuf message containing sliced layer data.
    def _onLayerMessage(self, message: Arcus.PythonMessage) -> None:
        self._stored_layer_data.append(message)

    ##  Called when an optimized sliced layer data message is received from the engine.
    #
    #   \param message The protobuf message containing sliced layer data.
    def _onOptimizedLayerMessage(self, message: Arcus.PythonMessage) -> None:
        if self._start_slice_job_build_plate is not None:
            if self._start_slice_job_build_plate not in self._stored_optimized_layer_data:
                self._stored_optimized_layer_data[
                    self._start_slice_job_build_plate] = []
            self._stored_optimized_layer_data[
                self._start_slice_job_build_plate].append(message)

    ##  Called when a progress message is received from the engine.
    #
    #   \param message The protobuf message containing the slicing progress.
    def _onProgressMessage(self, message: Arcus.PythonMessage) -> None:
        self.processingProgress.emit(message.amount)
        self.setState(BackendState.Processing)

    def _invokeSlice(self) -> None:
        if self._use_timer:
            # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
            # otherwise business as usual
            if self._machine_error_checker is None:
                self._change_timer.stop()
                return

            if self._machine_error_checker.needToWaitForResult:
                self._change_timer.stop()
            else:
                self._change_timer.start()

    ##  Called when the engine sends a message that slicing is finished.
    #
    #   \param message The protobuf message signalling that slicing is finished.
    def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None:
        self.setState(BackendState.Done)
        self.processingProgress.emit(1.0)

        gcode_list = self._scene.gcode_dict[
            self.
            _start_slice_job_build_plate]  #type: ignore #Because we generate this attribute dynamically.
        for index, line in enumerate(gcode_list):
            replaced = line.replace(
                "{print_time}",
                str(self._application.getPrintInformation().currentPrintTime.
                    getDisplayString(DurationFormat.Format.ISO8601)))
            replaced = replaced.replace(
                "{filament_amount}",
                str(self._application.getPrintInformation().materialLengths))
            replaced = replaced.replace(
                "{filament_weight}",
                str(self._application.getPrintInformation().materialWeights))
            replaced = replaced.replace(
                "{filament_cost}",
                str(self._application.getPrintInformation().materialCosts))
            replaced = replaced.replace(
                "{jobname}",
                str(self._application.getPrintInformation().jobName))

            gcode_list[index] = replaced

        self._slicing = False
        if self._slice_start_time:
            Logger.log("d", "Slicing took %s seconds",
                       time() - self._slice_start_time)
        Logger.log("d", "Number of models per buildplate: %s",
                   dict(self._numObjectsPerBuildPlate()))

        # See if we need to process the sliced layers job.
        active_build_plate = self._application.getMultiBuildPlateModel(
        ).activeBuildPlate
        if (self._layer_view_active
                and (self._process_layers_job is None
                     or not self._process_layers_job.isRunning())
                and active_build_plate == self._start_slice_job_build_plate
                and active_build_plate not in self._build_plates_to_be_sliced):

            self._startProcessSlicedLayersJob(active_build_plate)
        # self._onActiveViewChanged()
        self._start_slice_job_build_plate = None

        Logger.log("d", "See if there is more to slice...")
        # Somehow this results in an Arcus Error
        # self.slice()
        # Call slice again using the timer, allowing the backend to restart
        if self._build_plates_to_be_sliced:
            self.enableTimer(
            )  # manually enable timer to be able to invoke slice, also when in manual slice mode
            self._invokeSlice()

    ##  Called when a g-code message is received from the engine.
    #
    #   \param message The protobuf message containing g-code, encoded as UTF-8.
    def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None:
        self._scene.gcode_dict[self._start_slice_job_build_plate].append(
            message.data.decode("utf-8", "replace")
        )  #type: ignore #Because we generate this attribute dynamically.

    ##  Called when a g-code prefix message is received from the engine.
    #
    #   \param message The protobuf message containing the g-code prefix,
    #   encoded as UTF-8.
    def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None:
        self._scene.gcode_dict[self._start_slice_job_build_plate].insert(
            0, message.data.decode("utf-8", "replace")
        )  #type: ignore #Because we generate this attribute dynamically.

    ##  Creates a new socket connection.
    def _createSocket(self, protocol_file: str = None) -> None:
        if not protocol_file:
            plugin_path = PluginRegistry.getInstance().getPluginPath(
                self.getPluginId())
            if not plugin_path:
                Logger.log("e", "Could not get plugin path!",
                           self.getPluginId())
                return
            protocol_file = os.path.abspath(
                os.path.join(plugin_path, "Cura.proto"))
        super()._createSocket(protocol_file)
        self._engine_is_fresh = True

    ##  Called when anything has changed to the stuff that needs to be sliced.
    #
    #   This indicates that we should probably re-slice soon.
    def _onChanged(self, *args: Any, **kwargs: Any) -> None:
        self.needsSlicing()
        if self._use_timer:
            # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
            # otherwise business as usual
            if self._machine_error_checker is None:
                self._change_timer.stop()
                return

            if self._machine_error_checker.needToWaitForResult:
                self._change_timer.stop()
            else:
                self._change_timer.start()

    ##  Called when a print time message is received from the engine.
    #
    #   \param message The protobuf message containing the print time per feature and
    #   material amount per extruder
    def _onPrintTimeMaterialEstimates(self,
                                      message: Arcus.PythonMessage) -> None:
        material_amounts = []
        for index in range(message.repeatedMessageCount("materialEstimates")):
            material_amounts.append(
                message.getRepeatedMessage("materialEstimates",
                                           index).material_amount)

        times = self._parseMessagePrintTimes(message)
        self.printDurationMessage.emit(self._start_slice_job_build_plate,
                                       times, material_amounts)

    ##  Called for parsing message to retrieve estimated time per feature
    #
    #   \param message The protobuf message containing the print time per feature
    def _parseMessagePrintTimes(
            self, message: Arcus.PythonMessage) -> Dict[str, float]:
        result = {
            "inset_0": message.time_inset_0,
            "inset_x": message.time_inset_x,
            "skin": message.time_skin,
            "infill": message.time_infill,
            "support_infill": message.time_support_infill,
            "support_interface": message.time_support_interface,
            "support": message.time_support,
            "skirt": message.time_skirt,
            "travel": message.time_travel,
            "retract": message.time_retract,
            "none": message.time_none
        }
        return result

    ##  Called when the back-end connects to the front-end.
    def _onBackendConnected(self) -> None:
        if self._restart:
            self._restart = False
            self._onChanged()

    ##  Called when the user starts using some tool.
    #
    #   When the user starts using a tool, we should pause slicing to prevent
    #   continuously slicing while the user is dragging some tool handle.
    #
    #   \param tool The tool that the user is using.
    def _onToolOperationStarted(self, tool: Tool) -> None:
        self._tool_active = True  # Do not react on scene change
        self.disableTimer()
        # Restart engine as soon as possible, we know we want to slice afterwards
        if not self._engine_is_fresh:
            self._terminate()
            self._createSocket()

    ##  Called when the user stops using some tool.
    #
    #   This indicates that we can safely start slicing again.
    #
    #   \param tool The tool that the user was using.
    def _onToolOperationStopped(self, tool: Tool) -> None:
        self._tool_active = False  # React on scene change again
        self.determineAutoSlicing()  # Switch timer on if appropriate
        # Process all the postponed scene changes
        while self._postponed_scene_change_sources:
            source = self._postponed_scene_change_sources.pop(0)
            self._onSceneChanged(source)

    def _startProcessSlicedLayersJob(self, build_plate_number: int) -> None:
        self._process_layers_job = ProcessSlicedLayersJob(
            self._stored_optimized_layer_data[build_plate_number])
        self._process_layers_job.setBuildPlate(build_plate_number)
        self._process_layers_job.finished.connect(
            self._onProcessLayersFinished)
        self._process_layers_job.start()

    ##  Called when the user changes the active view mode.
    def _onActiveViewChanged(self) -> None:
        view = self._application.getController().getActiveView()
        if view:
            active_build_plate = self._application.getMultiBuildPlateModel(
            ).activeBuildPlate
            if view.getPluginId(
            ) == "SimulationView":  # If switching to layer view, we should process the layers if that hasn't been done yet.
                self._layer_view_active = True
                # There is data and we're not slicing at the moment
                # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
                # TODO: what build plate I am slicing
                if (active_build_plate in self._stored_optimized_layer_data
                        and not self._slicing and not self._process_layers_job
                        and active_build_plate
                        not in self._build_plates_to_be_sliced):

                    self._startProcessSlicedLayersJob(active_build_plate)
            else:
                self._layer_view_active = False

    ##  Called when the back-end self-terminates.
    #
    #   We should reset our state and start listening for new connections.
    def _onBackendQuit(self) -> None:
        if not self._restart:
            if self._process:  # type: ignore
                Logger.log(
                    "d",
                    "Backend quit with return code %s. Resetting process and socket.",
                    self._process.wait())  # type: ignore
                self._process = None  # type: ignore

    ##  Called when the global container stack changes
    def _onGlobalStackChanged(self) -> None:
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.disconnect(
                self._onSettingChanged)
            self._global_container_stack.containersChanged.disconnect(
                self._onChanged)
            extruders = list(self._global_container_stack.extruders.values())

            for extruder in extruders:
                extruder.propertyChanged.disconnect(self._onSettingChanged)
                extruder.containersChanged.disconnect(self._onChanged)

        self._global_container_stack = self._application.getMachineManager(
        ).activeMachine

        if self._global_container_stack:
            self._global_container_stack.propertyChanged.connect(
                self._onSettingChanged
            )  # Note: Only starts slicing when the value changed.
            self._global_container_stack.containersChanged.connect(
                self._onChanged)
            extruders = list(self._global_container_stack.extruders.values())
            for extruder in extruders:
                extruder.propertyChanged.connect(self._onSettingChanged)
                extruder.containersChanged.connect(self._onChanged)
            self._onChanged()

    def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None:
        if job.getBuildPlate() in self._stored_optimized_layer_data:
            del self._stored_optimized_layer_data[job.getBuildPlate()]
        else:
            Logger.log(
                "w",
                "The optimized layer data was already deleted for buildplate %s",
                job.getBuildPlate())
        self._process_layers_job = None
        Logger.log("d", "See if there is more to slice(2)...")
        self._invokeSlice()

    ##  Connect slice function to timer.
    def enableTimer(self) -> None:
        if not self._use_timer:
            self._change_timer.timeout.connect(self.slice)
            self._use_timer = True

    ##  Disconnect slice function from timer.
    #   This means that slicing will not be triggered automatically
    def disableTimer(self) -> None:
        if self._use_timer:
            self._use_timer = False
            self._change_timer.timeout.disconnect(self.slice)

    def _onPreferencesChanged(self, preference: str) -> None:
        if preference != "general/auto_slice":
            return
        auto_slice = self.determineAutoSlicing()
        if auto_slice:
            self._change_timer.start()

    ##   Tickle the backend so in case of auto slicing, it starts the timer.
    def tickle(self) -> None:
        if self._use_timer:
            self._change_timer.start()

    def _extruderChanged(self) -> None:
        if not self._multi_build_plate_model:
            Logger.log(
                "w",
                "CuraEngineBackend does not have multi_build_plate_model assigned!"
            )
            return
        for build_plate_number in range(
                self._multi_build_plate_model.maxBuildPlate + 1):
            if build_plate_number not in self._build_plates_to_be_sliced:
                self._build_plates_to_be_sliced.append(build_plate_number)
        self._invokeSlice()
class DuetRRFOutputDevice(OutputDevice):
    def __init__(self, name, url, duet_password, http_user, http_password,
                 device_type):
        self._device_type = device_type
        if device_type == DeviceType.print:
            description = catalog.i18nc("@action:button",
                                        "Print on {0}").format(name)
            name_id = name + "-print"
            priority = 30
        elif device_type == DeviceType.simulate:
            description = catalog.i18nc("@action:button",
                                        "Simulate on {0}").format(name)
            name_id = name + "-simulate"
            priority = 20
        elif device_type == DeviceType.upload:
            description = catalog.i18nc("@action:button",
                                        "Upload to {0}").format(name)
            name_id = name + "-upload"
            priority = 10
        else:
            assert False

        super().__init__(name_id)
        self.setShortDescription(description)
        self.setDescription(description)
        self.setPriority(priority)

        self._stage = OutputStage.ready
        self._name = name
        self._name_id = name_id
        self._device_type = device_type
        self._url = url
        self._duet_password = duet_password
        self._http_user = http_user
        self._http_password = http_password

        Logger.log("d", self._name_id + " | New DuetRRFOutputDevice created")
        Logger.log("d", self._name_id + " | URL: " + self._url)
        Logger.log(
            "d", self._name_id + " | Duet password: "******"set." if self._duet_password else "empty."))
        Logger.log(
            "d", self._name_id + " | HTTP Basic Auth user: "******"set." if self._http_user else "empty."))
        Logger.log(
            "d", self._name_id + " | HTTP Basic Auth password: "******"set." if self._http_password else "empty."))

        self._qnam = QtNetwork.QNetworkAccessManager()

        self._stream = None
        self._cleanupRequest()

        if hasattr(self, '_message'):
            self._message.hide()
        self._message = None

    def _timestamp(self):
        return ("time", datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'))

    def _send(self, command, query=None, next_stage=None, data=None):
        url = "{}rr_{}".format(self._url, command)

        if not query:
            query = dict()
        enc_query = urllib.parse.urlencode(query, quote_via=urllib.parse.quote)
        if enc_query:
            url += '?' + enc_query

        self._request = QtNetwork.QNetworkRequest(QUrl(url))
        self._request.setRawHeader(b'User-Agent', b'Cura Plugin DuetRRF')
        self._request.setRawHeader(b'Accept',
                                   b'application/json, text/javascript')
        self._request.setRawHeader(b'Connection', b'keep-alive')

        if self._http_user and self._http_password:
            self._request.setRawHeader(
                b'Authorization', b'Basic ' + base64.b64encode("{}:{}".format(
                    self._http_user, self._http_password).encode()))

        if data:
            self._request.setRawHeader(b'Content-Type',
                                       b'application/octet-stream')
            self._reply = self._qnam.post(self._request, data)
            self._reply.uploadProgress.connect(self._onUploadProgress)
        else:
            self._reply = self._qnam.get(self._request)

        if next_stage:
            self._reply.finished.connect(next_stage)
        self._reply.error.connect(self._onNetworkError)

    def requestWrite(self, node, fileName=None, *args, **kwargs):
        if self._stage != OutputStage.ready:
            raise OutputDeviceError.DeviceBusyError()

        if fileName:
            fileName = os.path.splitext(fileName)[0] + '.gcode'
        else:
            fileName = "%s.gcode" % Application.getInstance(
            ).getPrintInformation().jobName
        self._fileName = fileName

        path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                            'resources', 'qml', 'UploadFilename.qml')
        self._dialog = CuraApplication.getInstance().createQmlComponent(
            path, {"manager": self})
        self._dialog.textChanged.connect(self.onFilenameChanged)
        self._dialog.accepted.connect(self.onFilenameAccepted)
        self._dialog.show()
        self._dialog.findChild(QObject, "nameField").setProperty(
            'text', self._fileName)
        self._dialog.findChild(QObject,
                               "nameField").select(0,
                                                   len(self._fileName) - 6)
        self._dialog.findChild(QObject, "nameField").setProperty('focus', True)

    def onFilenameChanged(self):
        fileName = self._dialog.findChild(
            QObject, "nameField").property('text').strip()

        forbidden_characters = "\"'´`<>()[]?*\,;:&%#$!"
        for forbidden_character in forbidden_characters:
            if forbidden_character in fileName:
                self._dialog.setProperty('validName', False)
                self._dialog.setProperty(
                    'validationError',
                    'Filename cannot contain {}'.format(forbidden_characters))
                return

        if fileName == '.' or fileName == '..':
            self._dialog.setProperty('validName', False)
            self._dialog.setProperty('validationError',
                                     'Filename cannot be "." or ".."')
            return

        self._dialog.setProperty('validName', len(fileName) > 0)
        self._dialog.setProperty('validationError', 'Filename too short')

    def onFilenameAccepted(self):
        self._fileName = self._dialog.findChild(
            QObject, "nameField").property('text').strip()
        if not self._fileName.endswith('.gcode') and '.' not in self._fileName:
            self._fileName += '.gcode'
        Logger.log("d",
                   self._name_id + " | Filename set to: " + self._fileName)

        self._dialog.deleteLater()

        # create the temp file for the gcode
        self._stream = StringIO()
        self._stage = OutputStage.writing
        self.writeStarted.emit(self)

        # show a progress message
        self._message = Message(
            catalog.i18nc("@info:progress",
                          "Uploading to {}").format(self._name), 0, False, -1)
        self._message.show()

        Logger.log("d", self._name_id + " | Loading gcode...")

        # get the g-code through the GCodeWrite plugin
        # this serializes the actual scene and should produce the same output as "Save to File"
        gcode_writer = cast(
            MeshWriter,
            PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
        success = gcode_writer.write(self._stream, None)
        if not success:
            Logger.log("e", "GCodeWrite failed.")
            return

        # start
        Logger.log("d", self._name_id + " | Connecting...")
        self._send('connect', [("password", self._duet_password),
                               self._timestamp()], self.onUploadReady)

    def onUploadReady(self):
        if self._stage != OutputStage.writing:
            return

        Logger.log("d", self._name_id + " | Uploading...")
        self._stream.seek(0)
        self._postData = QByteArray()
        self._postData.append(self._stream.getvalue().encode())
        self._send('upload', [("name", "0:/gcodes/" + self._fileName),
                              self._timestamp()], self.onUploadDone,
                   self._postData)

    def onUploadDone(self):
        if self._stage != OutputStage.writing:
            return

        Logger.log("d", self._name_id + " | Upload done")

        self._stream.close()
        self.stream = None

        if self._device_type == DeviceType.simulate:
            Logger.log("d", self._name_id + " | Simulating...")
            if self._message:
                self._message.hide()
            self._message = Message(
                catalog.i18nc(
                    "@info:progress",
                    "Simulating print on {}...\nPLEASE CLOSE DWC AND DO NOT INTERACT WITH THE PRINTER!"
                ).format(self._name), 0, False, -1)
            self._message.show()

            self._send('gcode',
                       [("gcode", 'M37 P"0:/gcodes/' + self._fileName + '"')],
                       self.onSimulationPrintStarted)
        elif self._device_type == DeviceType.print:
            self.onReadyToPrint()
        elif self._device_type == DeviceType.upload:
            self._send('disconnect')
            if self._message:
                self._message.hide()
            text = "Uploaded file {} to {}.".format(
                os.path.basename(self._fileName), self._name)
            self._message = Message(catalog.i18nc("@info:status", text), 0,
                                    False)
            self._message.addAction(
                "open_browser", catalog.i18nc("@action:button",
                                              "Open Browser"), "globe",
                catalog.i18nc("@info:tooltip",
                              "Open browser to DuetWebControl."))
            self._message.actionTriggered.connect(
                self._onMessageActionTriggered)
            self._message.show()

            self.writeSuccess.emit(self)
            self._cleanupRequest()

    def onReadyToPrint(self):
        if self._stage != OutputStage.writing:
            return

        Logger.log("d", self._name_id + " | Ready to print")
        self._send('gcode',
                   [("gcode", 'M32 "0:/gcodes/' + self._fileName + '"')],
                   self.onPrintStarted)

    def onPrintStarted(self):
        if self._stage != OutputStage.writing:
            return

        Logger.log("d", self._name_id + " | Print started")

        self._send('disconnect')
        if self._message:
            self._message.hide()
        text = "Print started on {} with file {}".format(
            self._name, self._fileName)
        self._message = Message(catalog.i18nc("@info:status", text), 0, False)
        self._message.addAction(
            "open_browser", catalog.i18nc("@action:button", "Open Browser"),
            "globe",
            catalog.i18nc("@info:tooltip", "Open browser to DuetWebControl."))
        self._message.actionTriggered.connect(self._onMessageActionTriggered)
        self._message.show()

        self.writeSuccess.emit(self)
        self._cleanupRequest()

    def onSimulationPrintStarted(self):
        if self._stage != OutputStage.writing:
            return

        Logger.log(
            "d", self._name_id + " | Simulation print started for file " +
            self._fileName)

        # give it some to start the simulation
        QTimer.singleShot(15000, self.onCheckStatus)

    def onCheckStatus(self):
        if self._stage != OutputStage.writing:
            return

        Logger.log("d", self._name_id + " | Checking status...")

        self._send('status', [("type", "3")], self.onStatusReceived)

    def onStatusReceived(self):
        if self._stage != OutputStage.writing:
            return

        reply_body = bytes(self._reply.readAll()).decode()
        Logger.log("d", self._name_id + " | Status received | " + reply_body)

        status = json.loads(reply_body)
        if status["status"] in ['P', 'M']:
            # still simulating
            # RRF 1.21RC2 and earlier used P while simulating
            # RRF 1.21RC3 and later uses M while simulating
            if self._message and "fractionPrinted" in status:
                self._message.setProgress(float(status["fractionPrinted"]))
            QTimer.singleShot(5000, self.onCheckStatus)
        else:
            Logger.log("d", self._name_id + " | Simulation print finished")
            self._send('reply', [], self.onReported)

    def onReported(self):
        if self._stage != OutputStage.writing:
            return

        reply_body = bytes(self._reply.readAll()).decode().strip()
        Logger.log("d", self._name_id + " | Reported | " + reply_body)

        if self._message:
            self._message.hide()

        text = "Simulation finished on {}:\n\n{}".format(
            self._name, reply_body)
        self._message = Message(catalog.i18nc("@info:status", text), 0, False)
        self._message.addAction(
            "open_browser", catalog.i18nc("@action:button", "Open Browser"),
            "globe",
            catalog.i18nc("@info:tooltip", "Open browser to DuetWebControl."))
        self._message.actionTriggered.connect(self._onMessageActionTriggered)
        self._message.show()

        self._send('disconnect')
        self.writeSuccess.emit(self)
        self._cleanupRequest()

    def _onProgress(self, progress):
        if self._message:
            self._message.setProgress(progress)
        self.writeProgress.emit(self, progress)

    def _cleanupRequest(self):
        self._reply = None
        self._request = None
        if self._stream:
            self._stream.close()
        self._stream = None
        self._stage = OutputStage.ready
        self._fileName = None

    def _onMessageActionTriggered(self, message, action):
        if action == "open_browser":
            QDesktopServices.openUrl(QUrl(self._url))
            if self._message:
                self._message.hide()
            self._message = None

    def _onUploadProgress(self, bytesSent, bytesTotal):
        if bytesTotal > 0:
            self._onProgress(int(bytesSent * 100 / bytesTotal))

    def _onNetworkError(self, errorCode):
        Logger.log("e", "_onNetworkError: %s", repr(errorCode))
        if self._message:
            self._message.hide()
        self._message = None

        if self._reply:
            errorString = self._reply.errorString()
        else:
            errorString = ''
        message = Message(
            catalog.i18nc("@info:status",
                          "There was a network error: {} {}").format(
                              errorCode, errorString), 0, False)
        message.show()

        self.writeError.emit(self)
        self._cleanupRequest()
Exemple #42
0
class AuthorizationService:
    """The authorization service is responsible for handling the login flow, storing user credentials and providing
    account information.
    """

    # Emit signal when authentication is completed.
    onAuthStateChanged = Signal()

    # Emit signal when authentication failed.
    onAuthenticationError = Signal()

    accessTokenChanged = Signal()

    def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None:
        self._settings = settings
        self._auth_helpers = AuthorizationHelpers(settings)
        self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL)
        self._auth_data = None  # type: Optional[AuthenticationResponse]
        self._user_profile = None  # type: Optional["UserProfile"]
        self._preferences = preferences
        self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True)

        self._unable_to_get_data_message = None  # type: Optional[Message]

        self.onAuthStateChanged.connect(self._authChanged)

    def _authChanged(self, logged_in):
        if logged_in and self._unable_to_get_data_message is not None:
            self._unable_to_get_data_message.hide()

    def initialize(self, preferences: Optional["Preferences"] = None) -> None:
        if preferences is not None:
            self._preferences = preferences
        if self._preferences:
            self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")

    def getUserProfile(self) -> Optional["UserProfile"]:
        """Get the user profile as obtained from the JWT (JSON Web Token).

        If the JWT is not yet parsed, calling this will take care of that.

        :return: UserProfile if a user is logged in, None otherwise.

        See also: :py:method:`cura.OAuth2.AuthorizationService.AuthorizationService._parseJWT`
        """

        if not self._user_profile:
            # If no user profile was stored locally, we try to get it from JWT.
            try:
                self._user_profile = self._parseJWT()
            except requests.exceptions.ConnectionError:
                # Unable to get connection, can't login.
                Logger.logException("w", "Unable to validate user data with the remote server.")
                return None

        if not self._user_profile and self._auth_data:
            # If there is still no user profile from the JWT, we have to log in again.
            Logger.log("w", "The user profile could not be loaded. The user must log in again!")
            self.deleteAuthData()
            return None

        return self._user_profile

    def _parseJWT(self) -> Optional["UserProfile"]:
        """Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.

        :return: UserProfile if it was able to parse, None otherwise.
        """

        if not self._auth_data or self._auth_data.access_token is None:
            # If no auth data exists, we should always log in again.
            Logger.log("d", "There was no auth data or access token")
            return None
        user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
        if user_data:
            # If the profile was found, we return it immediately.
            return user_data
        # The JWT was expired or invalid and we should request a new one.
        if self._auth_data.refresh_token is None:
            Logger.log("w", "There was no refresh token in the auth data.")
            return None
        self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
        if not self._auth_data or self._auth_data.access_token is None:
            Logger.log("w", "Unable to use the refresh token to get a new access token.")
            # The token could not be refreshed using the refresh token. We should login again.
            return None
        # Ensure it gets stored as otherwise we only have it in memory. The stored refresh token has been deleted
        # from the server already. Do not store the auth_data if we could not get new auth_data (eg due to a
        # network error), since this would cause an infinite loop trying to get new auth-data
        if self._auth_data.success:
            self._storeAuthData(self._auth_data)
        return self._auth_helpers.parseJWT(self._auth_data.access_token)

    def getAccessToken(self) -> Optional[str]:
        """Get the access token as provided by the response data."""

        if self._auth_data is None:
            Logger.log("d", "No auth data to retrieve the access_token from")
            return None

        # Check if the current access token is expired and refresh it if that is the case.
        # We have a fallback on a date far in the past for currently stored auth data in cura.cfg.
        received_at = datetime.strptime(self._auth_data.received_at, TOKEN_TIMESTAMP_FORMAT) \
            if self._auth_data.received_at else datetime(2000, 1, 1)
        expiry_date = received_at + timedelta(seconds = float(self._auth_data.expires_in or 0) - 60)
        if datetime.now() > expiry_date:
            self.refreshAccessToken()

        return self._auth_data.access_token if self._auth_data else None

    def refreshAccessToken(self) -> None:
        """Try to refresh the access token. This should be used when it has expired."""

        if self._auth_data is None or self._auth_data.refresh_token is None:
            Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
            return
        response = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
        if response.success:
            self._storeAuthData(response)
            self.onAuthStateChanged.emit(logged_in = True)
        else:
            Logger.log("w", "Failed to get a new access token from the server.")
            self.onAuthStateChanged.emit(logged_in = False)

    def deleteAuthData(self) -> None:
        """Delete the authentication data that we have stored locally (eg; logout)"""

        if self._auth_data is not None:
            self._storeAuthData()
            self.onAuthStateChanged.emit(logged_in = False)

    def startAuthorizationFlow(self, force_browser_logout: bool = False) -> None:
        """Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login."""

        Logger.log("d", "Starting new OAuth2 flow...")

        # Create the tokens needed for the code challenge (PKCE) extension for OAuth2.
        # This is needed because the CuraDrivePlugin is a untrusted (open source) client.
        # More details can be found at https://tools.ietf.org/html/rfc7636.
        verification_code = self._auth_helpers.generateVerificationCode()
        challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code)

        state = AuthorizationHelpers.generateVerificationCode()

        # Create the query dict needed for the OAuth2 flow.
        query_parameters_dict = {
            "client_id": self._settings.CLIENT_ID,
            "redirect_uri": self._settings.CALLBACK_URL,
            "scope": self._settings.CLIENT_SCOPES,
            "response_type": "code",
            "state": state,  # Forever in our Hearts, RIP "(.Y.)" (2018-2020)
            "code_challenge": challenge_code,
            "code_challenge_method": "S512"
        }

        # Start a local web server to receive the callback URL on.
        try:
            self._server.start(verification_code, state)
        except OSError:
            Logger.logException("w", "Unable to create authorization request server")
            Message(i18n_catalog.i18nc("@info",
                                       "Unable to start a new sign in process. Check if another sign in attempt is still active."),
                    title=i18n_catalog.i18nc("@info:title", "Warning"),
                    message_type = Message.MessageType.WARNING).show()
            return

        auth_url = self._generate_auth_url(query_parameters_dict, force_browser_logout)
        # Open the authorization page in a new browser window.
        QDesktopServices.openUrl(QUrl(auth_url))

    def _generate_auth_url(self, query_parameters_dict: Dict[str, Optional[str]], force_browser_logout: bool) -> str:
        """
        Generates the authentications url based on the original auth_url and the query_parameters_dict to be included.
        If there is a request to force logging out of mycloud in the browser, the link to logoff from mycloud is
        prepended in order to force the browser to logoff from mycloud and then redirect to the authentication url to
        login again. This case is used to sync the accounts between Cura and the browser.

        :param query_parameters_dict: A dictionary with the query parameters to be url encoded and added to the
                                      authentication link
        :param force_browser_logout: If True, Cura will prepend the MYCLOUD_LOGOFF_URL link before the authentication
                                     link to force the a browser logout from mycloud.ultimaker.com
        :return: The authentication URL, properly formatted and encoded
        """
        auth_url = f"{self._auth_url}?{urlencode(query_parameters_dict)}"
        if force_browser_logout:
            connecting_char = "&" if "?" in MYCLOUD_LOGOFF_URL else "?"
            # The url after 'next=' should be urlencoded
            auth_url = f"{MYCLOUD_LOGOFF_URL}{connecting_char}next={quote_plus(auth_url)}"
        return auth_url

    def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
        """Callback method for the authentication flow."""
        if auth_response.success:
            Logger.log("d", "Got callback from Authorization state. The user should now be logged in!")
            self._storeAuthData(auth_response)
            self.onAuthStateChanged.emit(logged_in = True)
        else:
            Logger.log("d", "Got callback from Authorization state. Something went wrong: [%s]", auth_response.err_message)
            self.onAuthenticationError.emit(logged_in = False, error_message = auth_response.err_message)
        self._server.stop()  # Stop the web server at all times.

    def loadAuthDataFromPreferences(self) -> None:
        """Load authentication data from preferences."""
        Logger.log("d", "Attempting to load the auth data from preferences.")
        if self._preferences is None:
            Logger.log("e", "Unable to load authentication data, since no preference has been set!")
            return
        try:
            preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY))
            if preferences_data:
                self._auth_data = AuthenticationResponse(**preferences_data)
                # Also check if we can actually get the user profile information.
                user_profile = self.getUserProfile()
                if user_profile is not None:
                    self.onAuthStateChanged.emit(logged_in = True)
                    Logger.log("d", "Auth data was successfully loaded")
                else:
                    if self._unable_to_get_data_message is not None:
                        self._unable_to_get_data_message.hide()

                    self._unable_to_get_data_message = Message(i18n_catalog.i18nc("@info",
                                                                                  "Unable to reach the Ultimaker account server."),
                                                               title = i18n_catalog.i18nc("@info:title", "Warning"),
                                                               message_type = Message.MessageType.ERROR)
                    Logger.log("w", "Unable to load auth data from preferences")
                    self._unable_to_get_data_message.show()
        except (ValueError, TypeError):
            Logger.logException("w", "Could not load auth data from preferences")

    def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
        """Store authentication data in preferences."""

        Logger.log("d", "Attempting to store the auth data for [%s]", self._settings.OAUTH_SERVER_URL)
        if self._preferences is None:
            Logger.log("e", "Unable to save authentication data, since no preference has been set!")
            return

        self._auth_data = auth_data
        if auth_data:
            self._user_profile = self.getUserProfile()
            self._preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(auth_data.dump()))
        else:
            Logger.log("d", "Clearing the user profile")
            self._user_profile = None
            self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)

        self.accessTokenChanged.emit()
Exemple #43
0
class SliceInfo(Extension):
    info_url = "https://stats.youmagine.com/curastats/slice"

    def __init__(self):
        super().__init__()
        Application.getInstance().getOutputDeviceManager(
        ).writeStarted.connect(self._onWriteStarted)
        Preferences.getInstance().addPreference("info/send_slice_info", True)
        Preferences.getInstance().addPreference("info/asked_send_slice_info",
                                                False)

        if not Preferences.getInstance().getValue(
                "info/asked_send_slice_info"):
            self.send_slice_info_message = Message(catalog.i18nc(
                "@info",
                "Cura collects anonymised slicing statistics. You can disable this in preferences"
            ),
                                                   lifetime=0,
                                                   dismissable=False)
            self.send_slice_info_message.addAction(
                "Dismiss", catalog.i18nc("@action:button", "Dismiss"), None,
                "")
            self.send_slice_info_message.actionTriggered.connect(
                self.messageActionTriggered)
            self.send_slice_info_message.show()

    def messageActionTriggered(self, message_id, action_id):
        self.send_slice_info_message.hide()
        Preferences.getInstance().setValue("info/asked_send_slice_info", True)

    def _onWriteStarted(self, output_device):
        try:
            if not Preferences.getInstance().getValue("info/send_slice_info"):
                Logger.log("d", "'info/send_slice_info' is turned off.")
                return  # Do nothing, user does not want to send data

            # Listing all files placed on the buildplate
            modelhashes = []
            for node in DepthFirstIterator(CuraApplication.getInstance(
            ).getController().getScene().getRoot()):
                if node.callDecoration("isSliceable"):
                    modelhashes.append(node.getMeshData().getHash())

            # Creating md5sums and formatting them as discussed on JIRA
            modelhash_formatted = ",".join(modelhashes)

            global_container_stack = Application.getInstance(
            ).getGlobalContainerStack()

            # Get total material used (in mm^3)
            print_information = Application.getInstance().getPrintInformation()
            material_radius = 0.5 * global_container_stack.getProperty(
                "material_diameter", "value")

            # Send material per extruder
            material_used = [
                str(math.pi * material_radius * material_radius *
                    material_length)
                for material_length in print_information.materialLengths
            ]
            material_used = ",".join(material_used)

            containers = {"": global_container_stack.serialize()}
            for container in global_container_stack.getContainers():
                container_id = container.getId()
                try:
                    container_serialized = container.serialize()
                except NotImplementedError:
                    Logger.log("w", "Container %s could not be serialized!",
                               container_id)
                    continue
                if container_serialized:
                    containers[container_id] = container_serialized
                else:
                    Logger.log("i", "No data found in %s to be serialized!",
                               container_id)

            # Bundle the collected data
            submitted_data = {
                "processor":
                platform.processor(),
                "machine":
                platform.machine(),
                "platform":
                platform.platform(),
                "settings":
                json.dumps(
                    containers
                ),  # bundle of containers with their serialized contents
                "version":
                Application.getInstance().getVersion(),
                "modelhash":
                modelhash_formatted,
                "printtime":
                print_information.currentPrintTime.getDisplayString(
                    DurationFormat.Format.ISO8601),
                "filament":
                material_used,
                "language":
                Preferences.getInstance().getValue("general/language"),
            }

            # Convert data to bytes
            submitted_data = urllib.parse.urlencode(submitted_data)
            binary_data = submitted_data.encode("utf-8")

            # Sending slice info non-blocking
            reportJob = SliceInfoJob(self.info_url, binary_data)
            reportJob.start()
        except Exception as e:
            # We really can't afford to have a mistake here, as this would break the sending of g-code to a device
            # (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
            Logger.log(
                "e",
                "Exception raised while sending slice info: %s" % (repr(e))
            )  # But we should be notified about these problems of course.
Exemple #44
0
class RotateTool(Tool):
    def __init__(self):
        super().__init__()
        self._handle = RotateToolHandle.RotateToolHandle()

        self._snap_rotation = True
        self._snap_angle = math.radians(15)

        self._X_angle = 0.0
        self._Y_angle = 0.0
        self._Z_angle = 0.0

        self._angle = None
        self._angle_update_time = None

        self._shortcut_key = Qt.Key_Z

        self._progress_message = None
        self._iterations = 0
        self._total_iterations = 0
        self._rotating = False
        self.setExposedProperties("ToolHint", "RotationSnap",
                                  "RotationSnapAngle", "X", "Y", "Z")
        self._saved_node_positions = []

    ##  Handle mouse and keyboard events
    #
    #   \param event type(Event)
    def event(self, event):
        super().event(event)

        if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey:
            # Snap is toggled when pressing the shift button
            self._snap_rotation = (not self._snap_rotation)
            self.propertyChanged.emit()

        if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey:
            # Snap is "toggled back" when releasing the shift button
            self._snap_rotation = (not self._snap_rotation)
            self.propertyChanged.emit()

        if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(
        ):
            # Start a rotate operation
            if MouseEvent.LeftButton not in event.buttons:
                return False

            id = self._selection_pass.getIdAtPosition(event.x, event.y)
            if not id:
                return False

            if self._handle.isAxis(id):
                self.setLockedAxis(id)
            else:
                # Not clicked on an axis: do nothing.
                return False

            handle_position = self._handle.getWorldPosition()

            # Save the current positions of the node, as we want to rotate around their current centres
            self._saved_node_positions = []
            for node in Selection.getAllSelectedObjects():
                self._saved_node_positions.append((node, node.getPosition()))

            if id == ToolHandle.XAxis:
                self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x))
            elif id == ToolHandle.YAxis:
                self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y))
            #elif self._locked_axis == ToolHandle.ZAxis:
            elif id == ToolHandle.ZAxis:
                self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z))
            else:
                self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y))

            self.setDragStart(event.x, event.y)
            self._rotating = False
            self._angle = 0

        if event.type == Event.MouseMoveEvent:
            # Perform a rotate operation
            if not self.getDragPlane():
                return False

            if not self.getDragStart():
                self.setDragStart(event.x, event.y)
                if not self.getDragStart():  #May have set it to None.
                    return False

            if not self._rotating:
                self._rotating = True
                self.operationStarted.emit(self)

            handle_position = self._handle.getWorldPosition()

            drag_start = (self.getDragStart() - handle_position).normalized()
            drag_position = self.getDragPosition(event.x, event.y)
            if not drag_position:
                return
            drag_end = (drag_position - handle_position).normalized()

            try:
                angle = math.acos(drag_start.dot(drag_end))
            except ValueError:
                angle = 0

            if self._snap_rotation:
                angle = int(angle / self._snap_angle) * self._snap_angle
                if angle == 0:
                    return

            rotation = None
            if self.getLockedAxis() == ToolHandle.XAxis:
                direction = 1 if Vector.Unit_X.dot(
                    drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle,
                                                    Vector.Unit_X)
                self._X_angle = float(
                    self._X_angle) + direction * math.degrees(angle)
                for node in Selection.getAllSelectedObjects():
                    node._rotationX = self._X_angle
            elif self.getLockedAxis() == ToolHandle.YAxis:
                direction = 1 if Vector.Unit_Y.dot(
                    drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle,
                                                    Vector.Unit_Y)
                self._Y_angle = float(
                    self._Y_angle) + direction * math.degrees(angle)
                for node in Selection.getAllSelectedObjects():
                    node._rotationY = self._Y_angle
            elif self.getLockedAxis() == ToolHandle.ZAxis:
                direction = 1 if Vector.Unit_Z.dot(
                    drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle,
                                                    Vector.Unit_Z)
                self._Z_angle = float(
                    self._Z_angle) + direction * math.degrees(angle)
                for node in Selection.getAllSelectedObjects():
                    node._rotationZ = self._Z_angle
            else:
                direction = -1

            # Rate-limit the angle change notification
            # This is done to prevent the UI from being flooded with property change notifications,
            # which in turn would trigger constant repaints.
            new_time = time.monotonic()
            if not self._angle_update_time or new_time - self._angle_update_time > 0.1:
                self._angle_update_time = new_time
                self._angle += direction * angle
                self.propertyChanged.emit()

                # Rotate around the saved centeres of all selected nodes
                op = GroupedOperation()
                for node, position in self._saved_node_positions:
                    op.addOperation(
                        RotateOperation(node,
                                        rotation,
                                        rotate_around_point=position))
                op.push()

                self.setDragStart(event.x, event.y)

        if event.type == Event.MouseReleaseEvent:
            # Finish a rotate operation
            if self.getDragPlane():
                self.setDragPlane(None)
                self.setLockedAxis(None)
                self._angle = None
                self.propertyChanged.emit()
                if self._rotating:
                    self.operationStopped.emit(self)
                return True

    ##  Return a formatted angle of the current rotate operation
    #
    #   \return type(String) fully formatted string showing the angle by which the mesh(es) are rotated
    def getToolHint(self):
        return "%d°" % round(math.degrees(
            self._angle)) if self._angle else None

    ##  Get the state of the "snap rotation to N-degree increments" option
    #
    #   \return type(Boolean)
    def getRotationSnap(self):
        return self._snap_rotation

    ##  Set the state of the "snap rotation to N-degree increments" option
    #
    #   \param snap type(Boolean)
    def setRotationSnap(self, snap):
        if snap != self._snap_rotation:
            self._snap_rotation = snap
            self.propertyChanged.emit()

    ##  Get the number of degrees used in the "snap rotation to N-degree increments" option
    #
    #   \return type(Number)
    def getRotationSnapAngle(self):
        return self._snap_angle

    ##  Set the number of degrees used in the "snap rotation to N-degree increments" option
    #
    #   \param snap type(Number)
    def setRotationSnapAngle(self, angle):
        if angle != self._snap_angle:
            self._snap_angle = angle
            self.propertyChanged.emit()

    def _parseInt(self, str_value):
        try:
            parsed_value = float(str_value)
        except ValueError:
            parsed_value = float(0)
        return parsed_value

    ##  Get X
    #
    #   \return type(float)
    def getX(self):
        if Selection.getCount() > 1:
            self._X_angle = 0.0
            return self._X_angle
        self._X_angle = Selection.getAllSelectedObjects()[0]._rotationX
        return self._X_angle

    ##  Set X
    #
    #   \param X type(float)
    def setX(self, X):
        if float(X) != self._X_angle:
            self._angle = ((float(X) % 360) - (self._X_angle % 360)) % 360

            self._X_angle = float(X)

            #rotation = Quaternion.fromAngleAxis( math.radians( self._angle ), Vector.Unit_X)
            rotation = Quaternion()
            rotation.setByAngleAxis(math.radians(self._angle), Vector.Unit_X)

            # Save the current positions of the node, as we want to rotate around their current centres
            self._saved_node_positions = []
            for node in Selection.getAllSelectedObjects():
                self._saved_node_positions.append((node, node.getPosition()))
                node._rotationX = self._X_angle

            # Rate-limit the angle change notification
            # This is done to prevent the UI from being flooded with property change notifications,
            # which in turn would trigger constant repaints.
            new_time = time.monotonic()
            if not self._angle_update_time or new_time - self._angle_update_time > 0.1:
                self._angle_update_time = new_time

                # Rotate around the saved centeres of all selected nodes
                op = GroupedOperation()
                for node, position in self._saved_node_positions:
                    op.addOperation(
                        RotateOperation(node,
                                        rotation,
                                        rotate_around_point=position))
                op.push()

            self._angle = 0
            self.propertyChanged.emit()

    ##  Get Y
    #
    #   \return type(float)
    def getY(self):
        if Selection.getCount() > 1:
            self._Y_angle = 0.0
            return self._Y_angle
        self._Y_angle = Selection.getAllSelectedObjects()[0]._rotationY
        return self._Y_angle

    ##  Set Y
    #
    #   \param Y type(float)
    def setY(self, Y):
        if float(Y) != self._Y_angle:
            self._angle = ((float(Y) % 360) - (self._Y_angle % 360)) % 360

            self._Y_angle = float(Y)

            #rotation = Quaternion.fromAngleAxis(math.radians( self._angle ), Vector.Unit_Y)
            rotation = Quaternion()
            rotation.setByAngleAxis(math.radians(self._angle), Vector.Unit_Y)

            # Save the current positions of the node, as we want to rotate around their current centres
            self._saved_node_positions = []
            for node in Selection.getAllSelectedObjects():
                self._saved_node_positions.append((node, node.getPosition()))
                node._rotationY = self._Y_angle

            # Rate-limit the angle change notification
            # This is done to prevent the UI from being flooded with property change notifications,
            # which in turn would trigger constant repaints.
            new_time = time.monotonic()
            if not self._angle_update_time or new_time - self._angle_update_time > 0.1:
                self._angle_update_time = new_time

                # Rotate around the saved centeres of all selected nodes
                op = GroupedOperation()
                for node, position in self._saved_node_positions:
                    op.addOperation(
                        RotateOperation(node,
                                        rotation,
                                        rotate_around_point=position))
                op.push()

            self._angle = 0
            self.propertyChanged.emit()

    ##  Get Z
    #
    #   \return type(float)
    def getZ(self):
        if Selection.getCount() > 1:
            self._Z_angle = 0.0
            return self._Z_angle
        self._Z_angle = Selection.getAllSelectedObjects()[0]._rotationZ
        return self._Z_angle

    ##  Set Z
    #
    #   \param Z type(float)
    def setZ(self, Z):
        if float(Z) != self._Z_angle:
            self._angle = ((float(Z) % 360) - (self._Z_angle % 360)) % 360

            self._Z_angle = float(Z)

            #rotation = Quaternion.fromAngleAxis(math.radians( self._angle ), Vector.Unit_Z)
            rotation = Quaternion()
            rotation.setByAngleAxis(math.radians(self._angle), Vector.Unit_Z)

            # Save the current positions of the node, as we want to rotate around their current centres
            self._saved_node_positions = []
            for node in Selection.getAllSelectedObjects():
                self._saved_node_positions.append((node, node.getPosition()))
                node._rotationZ = self._Z_angle

            # Rate-limit the angle change notification
            # This is done to prevent the UI from being flooded with property change notifications,
            # which in turn would trigger constant repaints.
            new_time = time.monotonic()
            if not self._angle_update_time or new_time - self._angle_update_time > 0.1:
                self._angle_update_time = new_time

                # Rotate around the saved centeres of all selected nodes
                op = GroupedOperation()
                for node, position in self._saved_node_positions:
                    op.addOperation(
                        RotateOperation(node,
                                        rotation,
                                        rotate_around_point=position))
                op.push()

            self._angle = 0
            self.propertyChanged.emit()

    ##  Reset the orientation of the mesh(es) to their original orientation(s)
    # Remember Y is Z and Z is Y
    def resetRotation(self):
        for node in Selection.getAllSelectedObjects():
            node.setMirror(Vector(1, 1, 1))
            node._rotationX = 0.0
            node._rotationY = 0.0
            node._rotationZ = 0.0

        Selection.applyOperation(SetTransformOperation, None, Quaternion(),
                                 None)

        self._X_angle = 0
        self._Z_angle = 0
        self._Y_angle = 0
        self.propertyChanged.emit()

    ##  Initialise and start a LayFlatOperation
    #
    #   Note: The LayFlat functionality is mostly used for 3d printing and should probably be moved into the Cura project
    def layFlat(self):
        self.operationStarted.emit(self)
        self._progress_message = Message("Laying object flat on buildplate...",
                                         lifetime=0,
                                         dismissable=False,
                                         title="Object Rotation")
        self._progress_message.setProgress(0)

        self._iterations = 0
        self._total_iterations = 0
        for selected_object in Selection.getAllSelectedObjects():
            self._layObjectFlat(selected_object)

        self._progress_message.show()

        operations = Selection.applyOperation(LayFlatOperation)
        for op in operations:
            op.progress.connect(self._layFlatProgress)

        job = LayFlatJob(operations)
        job.finished.connect(self._layFlatFinished)
        job.start()

    ##  Lays the given object flat. The given object can be a group or not.
    def _layObjectFlat(self, selected_object):
        if not selected_object.callDecoration("isGroup"):
            self._total_iterations += len(
                selected_object.getMeshDataTransformed().getVertices()) * 2
        else:
            for child in selected_object.getChildren():
                self._layObjectFlat(child)

    ##  Called while performing the LayFlatOperation so progress can be shown
    #
    #   Note that the LayFlatOperation rate-limits these callbacks to prevent the UI from being flooded with property change notifications,
    #   \param iterations type(int) number of iterations performed since the last callback
    def _layFlatProgress(self, iterations):
        self._iterations += iterations
        self._progress_message.setProgress(100 * self._iterations /
                                           self._total_iterations)

    ##  Called when the LayFlatJob is done running all of its LayFlatOperations
    #
    #   \param job type(LayFlatJob)
    def _layFlatFinished(self, job):
        if self._progress_message:
            self._progress_message.hide()
            self._progress_message = None

        self.operationStopped.emit(self)
Exemple #45
0
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
    printJobsChanged = pyqtSignal()
    activePrinterChanged = pyqtSignal()
    activeCameraUrlChanged = pyqtSignal()
    receivedPrintJobsChanged = pyqtSignal()

    # Notify can only use signals that are defined by the class that they are in, not inherited ones.
    # Therefore we create a private signal used to trigger the printersChanged signal.
    _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._application = CuraApplication.getInstance()

        self._number_of_extruders = 2

        self._dummy_lambdas = (
            "", {}, io.BytesIO()
        )  # type: Tuple[Optional[str], Dict[str, Union[str, int, bool]], Union[io.StringIO, io.BytesIO]]

        self._print_jobs = [] # type: List[UM3PrintJobOutputModel]
        self._received_print_jobs = False # type: bool

        if PluginRegistry.getInstance() is not None:
            plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting")
            if plugin_path is None:
                Logger.log("e", "Cloud not find plugin path for plugin UM3NetworkPrnting")
                raise RuntimeError("Cloud not find plugin path for plugin UM3NetworkPrnting")
            self._monitor_view_qml_path = os.path.join(plugin_path, "resources", "qml", "MonitorStage.qml")

        # Trigger the printersChanged signal when the private signal is triggered
        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, filter_by_machine: bool = False, **kwargs) -> None:
        self.writeStarted.emit(self)

        self.sendMaterialProfiles()

        mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion)

        # This function pauses with the yield, waiting on instructions on which printer it needs to print with.
        if not mesh_format.is_valid:
            Logger.log("e", "Missing file or mesh writer!")
            return
        self._sending_job = self._sendPrintJob(mesh_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:
            if PluginRegistry.getInstance() is not None:
                path = os.path.join(
                    PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
                    "resources", "qml", "PrintWindow.qml"
                )
                self._printer_selection_dialog = self._application.createQmlComponent(path, {"OutputDevice": self})
        if self._printer_selection_dialog is not None:
            self._printer_selection_dialog.show()

    ##  Whether the printer that this output device represents supports print job actions via the local network.
    @pyqtProperty(bool, constant=True)
    def supportsPrintJobActions(self) -> bool:
        return True

    @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 mesh_format Object responsible for choosing the right kind of format to write with.
    def _sendPrintJob(self, mesh_format: MeshFormatHandler, 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

        # Potentially wait on the user to select a target printer.
        target_printer = yield  # type: Optional[str]

        # Using buffering greatly reduces the write time for many lines of gcode

        stream = mesh_format.createStream()

        job = WriteFileJob(mesh_format.writer, stream, nodes, mesh_format.file_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()

        if mesh_format.preferred_format is not None:
            self._dummy_lambdas = (target_printer, mesh_format.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 = "",
                                         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 = self._application.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

    ##  The IP address of the printer.
    @pyqtProperty(str, constant = True)
    def address(self) -> str:
        return self._address

    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 is not None 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 = "",
                                                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
            self._application.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":
            self._application.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 getTimeCompleted(self, time_remaining: int) -> str:
        return formatTimeCompleted(time_remaining)

    @pyqtSlot(int, result = str)
    def getDateCompleted(self, time_remaining: int) -> str:
        return formatDateCompleted(time_remaining)

    @pyqtSlot(int, result = str)
    def formatDuration(self, seconds: int) -> str:
        return Duration(seconds).getDisplayString(DurationFormat.Format.Short)

    @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)

    # Set the remote print job state.
    def setJobState(self, print_job_uuid: str, state: str) -> None:
        # We rewrite 'resume' to 'print' here because we are using the old print job action endpoints.
        action = "print" if state == "resume" else state
        data = "{\"action\": \"%s\"}" % action
        self.put("print_jobs/%s/action" % 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 = PrinterConfigurationModel()
        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)
        configuration.setPrinterType(data.get("machine_variant", ""))
        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 = self._application.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("material", ""),
                                        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"])

        if data["status"] != "unreachable":
            self._application.getDiscoveredPrintersModel().updateDiscoveredPrinter(data["ip_address"],
                                                                               name = data["friendly_name"],
                                                                               machine_type = 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.updateBuildplate(data["build_plate"]["type"])
        if not data["enabled"]:
            printer.updateState("disabled")
        else:
            printer.updateState(data["status"])

        for index in range(0, self._number_of_extruders):
            extruder = printer.extruders[index]
            try:
                extruder_data = data["configuration"][index]
            except IndexError:
                break

            extruder.updateHotendID(extruder_data.get("print_core_id", ""))

            material_data = extruder_data["material"]
            if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
                material = self._createMaterialOutputModel(material_data)
                extruder.updateActiveMaterial(material)

    def _removeJob(self, job: UM3PrintJobOutputModel) -> bool:
        if job not in self._print_jobs:
            return False

        if job.assignedPrinter:
            job.assignedPrinter.updateActivePrintJob(None)
            job.stateChanged.disconnect(self._printJobStateChanged)
        self._print_jobs.remove(job)

        return True

    def _removePrinter(self, printer: PrinterOutputModel) -> None:
        self._printers.remove(printer)
        if self._active_printer == printer:
            self._active_printer = None
            self.activePrinterChanged.emit()

    ##  Sync the material profiles in Cura with the printer.
    #
    #   This gets called when connecting to a printer as well as when sending a
    #   print.
    def sendMaterialProfiles(self) -> None:
        job = SendMaterialJob(device = self)
        job.run()
class AuthorizationService:
    # Emit signal when authentication is completed.
    onAuthStateChanged = Signal()

    # Emit signal when authentication failed.
    onAuthenticationError = Signal()

    def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None:
        self._settings = settings
        self._auth_helpers = AuthorizationHelpers(settings)
        self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL)
        self._auth_data = None  # type: Optional[AuthenticationResponse]
        self._user_profile = None  # type: Optional["UserProfile"]
        self._preferences = preferences
        self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True)

        self._unable_to_get_data_message = None  # type: Optional[Message]

        self.onAuthStateChanged.connect(self._authChanged)

    def _authChanged(self, logged_in):
        if logged_in and self._unable_to_get_data_message is not None:
            self._unable_to_get_data_message.hide()

    def initialize(self, preferences: Optional["Preferences"] = None) -> None:
        if preferences is not None:
            self._preferences = preferences
        if self._preferences:
            self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")

    ##  Get the user profile as obtained from the JWT (JSON Web Token).
    #   If the JWT is not yet parsed, calling this will take care of that.
    #   \return UserProfile if a user is logged in, None otherwise.
    #   \sa _parseJWT
    def getUserProfile(self) -> Optional["UserProfile"]:
        if not self._user_profile:
            # If no user profile was stored locally, we try to get it from JWT.
            try:
                self._user_profile = self._parseJWT()
            except requests.exceptions.ConnectionError:
                # Unable to get connection, can't login.
                return None

        if not self._user_profile and self._auth_data:
            # If there is still no user profile from the JWT, we have to log in again.
            Logger.log("w", "The user profile could not be loaded. The user must log in again!")
            self.deleteAuthData()
            return None

        return self._user_profile

    ##  Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
    #   \return UserProfile if it was able to parse, None otherwise.
    def _parseJWT(self) -> Optional["UserProfile"]:
        if not self._auth_data or self._auth_data.access_token is None:
            # If no auth data exists, we should always log in again.
            return None
        user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
        if user_data:
            # If the profile was found, we return it immediately.
            return user_data
        # The JWT was expired or invalid and we should request a new one.
        if self._auth_data.refresh_token is None:
            return None
        self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
        if not self._auth_data or self._auth_data.access_token is None:
            # The token could not be refreshed using the refresh token. We should login again.
            return None

        return self._auth_helpers.parseJWT(self._auth_data.access_token)

    ##  Get the access token as provided by the repsonse data.
    def getAccessToken(self) -> Optional[str]:
        if self._auth_data is None:
            Logger.log("d", "No auth data to retrieve the access_token from")
            return None

        # Check if the current access token is expired and refresh it if that is the case.
        # We have a fallback on a date far in the past for currently stored auth data in cura.cfg.
        received_at = datetime.strptime(self._auth_data.received_at, TOKEN_TIMESTAMP_FORMAT) \
            if self._auth_data.received_at else datetime(2000, 1, 1)
        expiry_date = received_at + timedelta(seconds = float(self._auth_data.expires_in or 0) - 60)
        if datetime.now() > expiry_date:
            self.refreshAccessToken()

        return self._auth_data.access_token if self._auth_data else None

    ##  Try to refresh the access token. This should be used when it has expired.
    def refreshAccessToken(self) -> None:
        if self._auth_data is None or self._auth_data.refresh_token is None:
            Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
            return
        response = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
        if response.success:
            self._storeAuthData(response)
            self.onAuthStateChanged.emit(logged_in = True)
        else:
            self.onAuthStateChanged.emit(logged_in = False)

    ##  Delete the authentication data that we have stored locally (eg; logout)
    def deleteAuthData(self) -> None:
        if self._auth_data is not None:
            self._storeAuthData()
            self.onAuthStateChanged.emit(logged_in = False)

    ##  Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.
    def startAuthorizationFlow(self) -> None:
        Logger.log("d", "Starting new OAuth2 flow...")

        # Create the tokens needed for the code challenge (PKCE) extension for OAuth2.
        # This is needed because the CuraDrivePlugin is a untrusted (open source) client.
        # More details can be found at https://tools.ietf.org/html/rfc7636.
        verification_code = self._auth_helpers.generateVerificationCode()
        challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code)

        # Create the query string needed for the OAuth2 flow.
        query_string = urlencode({
            "client_id": self._settings.CLIENT_ID,
            "redirect_uri": self._settings.CALLBACK_URL,
            "scope": self._settings.CLIENT_SCOPES,
            "response_type": "code",
            "state": "(.Y.)",
            "code_challenge": challenge_code,
            "code_challenge_method": "S512"
        })

        # Open the authorization page in a new browser window.
        webbrowser.open_new("{}?{}".format(self._auth_url, query_string))

        # Start a local web server to receive the callback URL on.
        self._server.start(verification_code)

    ##  Callback method for the authentication flow.
    def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
        if auth_response.success:
            self._storeAuthData(auth_response)
            self.onAuthStateChanged.emit(logged_in = True)
        else:
            self.onAuthenticationError.emit(logged_in = False, error_message = auth_response.err_message)
        self._server.stop()  # Stop the web server at all times.

    ##  Load authentication data from preferences.
    def loadAuthDataFromPreferences(self) -> None:
        if self._preferences is None:
            Logger.log("e", "Unable to load authentication data, since no preference has been set!")
            return
        try:
            preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY))
            if preferences_data:
                self._auth_data = AuthenticationResponse(**preferences_data)
                # Also check if we can actually get the user profile information.
                user_profile = self.getUserProfile()
                if user_profile is not None:
                    self.onAuthStateChanged.emit(logged_in = True)
                else:
                    if self._unable_to_get_data_message is not None:
                        self._unable_to_get_data_message.hide()

                    self._unable_to_get_data_message = Message(i18n_catalog.i18nc("@info", "Unable to reach the Ultimaker account server."), title = i18n_catalog.i18nc("@info:title", "Warning"))
                    self._unable_to_get_data_message.addAction("retry", i18n_catalog.i18nc("@action:button", "Retry"), "[no_icon]", "[no_description]")
                    self._unable_to_get_data_message.actionTriggered.connect(self._onMessageActionTriggered)
                    self._unable_to_get_data_message.show()
        except ValueError:
            Logger.logException("w", "Could not load auth data from preferences")

    ##  Store authentication data in preferences.
    def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
        if self._preferences is None:
            Logger.log("e", "Unable to save authentication data, since no preference has been set!")
            return
        
        self._auth_data = auth_data
        if auth_data:
            self._user_profile = self.getUserProfile()
            self._preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(vars(auth_data)))
        else:
            self._user_profile = None
            self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)

    def _onMessageActionTriggered(self, _, action):
        if action == "retry":
            self.loadAuthDataFromPreferences()
    def exportProfile(self, instance_ids, file_name, file_type):
        # Parse the fileType to deduce what plugin can save the file format.
        # fileType has the format "<description> (*.<extension>)"
        split = file_type.rfind(" (*.")  # Find where the description ends and the extension starts.
        if split < 0:  # Not found. Invalid format.
            Logger.log("e", "Invalid file format identifier %s", file_type)
            return
        description = file_type[:split]
        extension = file_type[split + 4:-1]  # Leave out the " (*." and ")".
        if not file_name.endswith("." + extension):  # Auto-fill the extension if the user did not provide any.
            file_name += "." + extension

        # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself.
        if not Platform.isWindows():
            if os.path.exists(file_name):
                result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
                                              catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
                if result == QMessageBox.No:
                    return
        found_containers = []
        extruder_positions = []
        for instance_id in instance_ids:
            containers = ContainerRegistry.getInstance().findInstanceContainers(id = instance_id)
            if containers:
                found_containers.append(containers[0])

                # Determine the position of the extruder of this container
                extruder_id = containers[0].getMetaDataEntry("extruder", "")
                if extruder_id == "":
                    # Global stack
                    extruder_positions.append(-1)
                else:
                    extruder_containers = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = extruder_id)
                    if extruder_containers:
                        extruder_positions.append(int(extruder_containers[0].get("position", 0)))
                    else:
                        extruder_positions.append(0)
        # Ensure the profiles are always exported in order (global, extruder 0, extruder 1, ...)
        found_containers = [containers for (positions, containers) in sorted(zip(extruder_positions, found_containers))]

        profile_writer = self._findProfileWriter(extension, description)

        try:
            success = profile_writer.write(file_name, found_containers)
        except Exception as e:
            Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
            m = Message(catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)),
                        lifetime = 0,
                        title = catalog.i18nc("@info:title", "Error"))
            m.show()
            return
        if not success:
            Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
            m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name),
                        lifetime = 0,
                        title = catalog.i18nc("@info:title", "Error"))
            m.show()
            return
        m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Exported profile to <filename>{0}</filename>", file_name),
                    title = catalog.i18nc("@info:title", "Export succeeded"))
        m.show()
Exemple #48
0
class SliceInfo(Extension):
    info_url = ""

    def __init__(self):
        super().__init__()
        Application.getInstance().getOutputDeviceManager(
        ).writeStarted.connect(self._onWriteStarted)
        Preferences.getInstance().addPreference("info/send_slice_info", True)
        Preferences.getInstance().addPreference("info/asked_send_slice_info",
                                                False)

        if not Preferences.getInstance().getValue(
                "info/asked_send_slice_info"):
            self.send_slice_info_message = Message(catalog.i18nc(
                "@info", "BCN3D Cura collects anonymized usage statistics."),
                                                   lifetime=0,
                                                   dismissable=False,
                                                   title=catalog.i18nc(
                                                       "@info:title",
                                                       "Collecting Data"))

            self.send_slice_info_message.addAction(
                "Dismiss",
                name=catalog.i18nc("@action:button", "Allow"),
                icon=None,
                description=catalog.i18nc(
                    "@action:tooltip",
                    "Allow BCN3D Cura to send anonymized usage statistics to help prioritize future improvements to BCN3D Cura. Some of your preferences and settings are sent, the BCN3D Cura version and a hash of the models you're slicing."
                ))
            self.send_slice_info_message.addAction(
                "Disable",
                name=catalog.i18nc("@action:button", "Disable"),
                icon=None,
                description=catalog.i18nc(
                    "@action:tooltip",
                    "Don't allow BCN3D Cura to send anonymized usage statistics. You can enable it again in the preferences."
                ),
                button_style=Message.ActionButtonStyle.LINK)
            self.send_slice_info_message.actionTriggered.connect(
                self.messageActionTriggered)
            self.send_slice_info_message.show()

    ##  Perform action based on user input.
    #   Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it.
    def messageActionTriggered(self, message_id, action_id):
        Preferences.getInstance().setValue("info/asked_send_slice_info", True)
        if action_id == "Disable":
            CuraApplication.getInstance().showPreferences()
        self.send_slice_info_message.hide()

    def _onWriteStarted(self, output_device):
        try:
            if not Preferences.getInstance().getValue("info/send_slice_info"):
                Logger.log("d", "'info/send_slice_info' is turned off.")
                return  # Do nothing, user does not want to send data

            global_container_stack = Application.getInstance(
            ).getGlobalContainerStack()
            print_information = Application.getInstance().getPrintInformation()

            data = dict()  # The data that we're going to submit.
            data["time_stamp"] = time.time()
            data["cura_version"] = Application.getInstance().getVersion()

            active_mode = Preferences.getInstance().getValue(
                "cura/active_mode")
            if active_mode == 0:
                data["active_mode"] = "recommended"
            else:
                data["active_mode"] = "custom"

            definition_changes = global_container_stack.definitionChanges
            machine_settings_changed_by_user = False
            if definition_changes.getId() != "empty":
                # Now a definition_changes container will always be created for a stack,
                # so we also need to check if there is any instance in the definition_changes container
                if definition_changes.getAllKeys():
                    machine_settings_changed_by_user = True

            data[
                "machine_settings_changed_by_user"] = machine_settings_changed_by_user
            data["language"] = Preferences.getInstance().getValue(
                "general/language")
            data["os"] = {
                "type": platform.system(),
                "version": platform.version()
            }

            data["active_machine"] = {
                "definition_id":
                global_container_stack.definition.getId(),
                "manufacturer":
                global_container_stack.definition.getMetaData().get(
                    "manufacturer", "")
            }

            # add extruder specific data to slice info
            data["extruders"] = []
            extruders = list(ExtruderManager.getInstance().getMachineExtruders(
                global_container_stack.getId()))
            extruders = sorted(
                extruders,
                key=lambda extruder: extruder.getMetaDataEntry("position"))

            for extruder in extruders:
                extruder_dict = dict()
                extruder_dict["active"] = ExtruderManager.getInstance(
                ).getActiveExtruderStack() == extruder
                extruder_dict["material"] = {
                    "GUID": extruder.material.getMetaData().get("GUID", ""),
                    "type":
                    extruder.material.getMetaData().get("material", ""),
                    "brand": extruder.material.getMetaData().get("brand", ""),
                    "color":
                    extruder.material.getMetaData().get("color_name", "")
                }
                extruder_position = int(
                    extruder.getMetaDataEntry("position", "0"))
                if len(print_information.materialLengths) > extruder_position:
                    extruder_dict[
                        "material_used"] = print_information.materialLengths[
                            extruder_position]
                extruder_dict["variant"] = extruder.variant.getName()
                extruder_dict["nozzle_size"] = extruder.getProperty(
                    "machine_nozzle_size", "value")

                extruder_settings = dict()
                # extruder_settings["wall_line_count"] = extruder.getProperty("wall_line_count", "value")
                # extruder_settings["retraction_enable"] = extruder.getProperty("retraction_enable", "value")
                # extruder_settings["infill_sparse_density"] = extruder.getProperty("infill_sparse_density", "value")
                # extruder_settings["infill_pattern"] = extruder.getProperty("infill_pattern", "value")
                # extruder_settings["gradual_infill_steps"] = extruder.getProperty("gradual_infill_steps", "value")
                # extruder_settings["default_material_print_temperature"] = extruder.getProperty("default_material_print_temperature", "value")
                # extruder_settings["material_print_temperature"] = extruder.getProperty("material_print_temperature", "value")
                for key in extruder.getAllKeys():
                    if extruder.getProperty(key, "settable_per_extruder"):
                        extruder_settings[key] = extruder.getProperty(
                            key, "value")
                extruder_dict["extruder_settings"] = extruder_settings
                data["extruders"].append(extruder_dict)

            data[
                "quality_profile"] = global_container_stack.quality.getMetaData(
                ).get("quality_type")

            data["models"] = []
            # Listing all files placed on the build plate
            for node in DepthFirstIterator(CuraApplication.getInstance(
            ).getController().getScene().getRoot()):
                if node.callDecoration("isSliceable"):
                    model = dict()
                    model["hash"] = node.getMeshData().getHash()
                    # bounding_box = node.getBoundingBox()
                    # model["bounding_box"] = {"minimum": {"x": bounding_box.minimum.x,
                    #                                      "y": bounding_box.minimum.y,
                    #                                      "z": bounding_box.minimum.z},
                    #                          "maximum": {"x": bounding_box.maximum.x,
                    #                                      "y": bounding_box.maximum.y,
                    #                                      "z": bounding_box.maximum.z}}
                    # model["transformation"] = {"data": str(node.getWorldTransformation().getData()).replace("\n", "")}
                    # extruder_position = node.callDecoration("getActiveExtruderPosition")
                    # model["extruder"] = 0 if extruder_position is None else int(extruder_position)

                    # model_settings = dict()
                    # model_stack = node.callDecoration("getStack")
                    # if model_stack:
                    #     model_settings["support_enabled"] = model_stack.getProperty("support_enable", "value")
                    #     model_settings["support_extruder_nr"] = int(model_stack.getProperty("support_extruder_nr", "value"))
                    #
                    #     # Mesh modifiers;
                    #     model_settings["infill_mesh"] = model_stack.getProperty("infill_mesh", "value")
                    #     model_settings["cutting_mesh"] = model_stack.getProperty("cutting_mesh", "value")
                    #     model_settings["support_mesh"] = model_stack.getProperty("support_mesh", "value")
                    #     model_settings["anti_overhang_mesh"] = model_stack.getProperty("anti_overhang_mesh", "value")
                    #
                    #     model_settings["wall_line_count"] = model_stack.getProperty("wall_line_count", "value")
                    #     model_settings["retraction_enable"] = model_stack.getProperty("retraction_enable", "value")
                    #
                    #     # Infill settings
                    #     model_settings["infill_sparse_density"] = model_stack.getProperty("infill_sparse_density", "value")
                    #     model_settings["infill_pattern"] = model_stack.getProperty("infill_pattern", "value")
                    #     model_settings["gradual_infill_steps"] = model_stack.getProperty("gradual_infill_steps", "value")
                    #
                    # model["model_settings"] = model_settings

                    data["models"].append(model)

            print_times = print_information.printTimes()
            data["print_times"] = {
                "travel":
                int(print_times["travel"].getDisplayString(
                    DurationFormat.Format.Seconds)),
                "support":
                int(print_times["support"].getDisplayString(
                    DurationFormat.Format.Seconds)),
                "infill":
                int(print_times["infill"].getDisplayString(
                    DurationFormat.Format.Seconds)),
                "total":
                int(
                    print_information.currentPrintTime.getDisplayString(
                        DurationFormat.Format.Seconds))
            }

            print_settings = dict()
            for key in global_container_stack.getAllKeys():
                if not global_container_stack.getProperty(
                        key, "settable_per_extruder"):
                    print_settings[key] = global_container_stack.getProperty(
                        key, "value")
            # print_settings["layer_height"] = global_container_stack.getProperty("layer_height", "value")
            #
            # # Support settings
            # print_settings["support_enabled"] = global_container_stack.getProperty("support_enable", "value")
            # print_settings["support_extruder_nr"] = int(global_container_stack.getProperty("support_extruder_nr", "value"))
            #
            # # Platform adhesion settings
            # print_settings["adhesion_type"] = global_container_stack.getProperty("adhesion_type", "value")
            #
            # # Shell settings
            # print_settings["wall_line_count"] = global_container_stack.getProperty("wall_line_count", "value")
            # print_settings["retraction_enable"] = global_container_stack.getProperty("retraction_enable", "value")
            #
            # # Prime tower settings
            # print_settings["prime_tower_enable"] = global_container_stack.getProperty("prime_tower_enable", "value")
            #
            # # Infill settings
            # print_settings["infill_sparse_density"] = global_container_stack.getProperty("infill_sparse_density", "value")
            # print_settings["infill_pattern"] = global_container_stack.getProperty("infill_pattern", "value")
            # print_settings["gradual_infill_steps"] = global_container_stack.getProperty("gradual_infill_steps", "value")
            #
            # print_settings["print_sequence"] = global_container_stack.getProperty("print_sequence", "value")

            data["print_settings"] = print_settings

            # Send the name of the output device type that is used.
            data["output_to"] = type(output_device).__name__

            # Convert data to bytes
            json_data = json.dumps(data)
            self._writeToGCode(json_data)

            binary_data = json.dumps(data).encode("utf-8")

            # Sending slice info non-blocking
            reportJob = SliceInfoJob(self.info_url, binary_data)
            reportJob.start()
        except Exception:
            # We really can't afford to have a mistake here, as this would break the sending of g-code to a device
            # (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
            Logger.logException(
                "e", "Exception raised while sending slice info."
            )  # But we should be notified about these problems of course.

    def _writeToGCode(self, json_data):
        active_build_plate_id = Application.getInstance().getBuildPlateModel(
        ).activeBuildPlate
        gcode_dict = getattr(
            Application.getInstance().getController().getScene(), "gcode_dict")
        gcode_list = gcode_dict[active_build_plate_id]
        if not any(";PrintInfo/" in s for s in gcode_list):
            gcode_list.append("\n\n;PrintInfo/" + str(json_data) +
                              "/PrintInfo\n")
Exemple #49
0
class SimulationView(CuraView):
    # Must match SimulationViewMenuComponent.qml
    LAYER_VIEW_TYPE_MATERIAL_TYPE = 0
    LAYER_VIEW_TYPE_LINE_TYPE = 1
    LAYER_VIEW_TYPE_FEEDRATE = 2
    LAYER_VIEW_TYPE_THICKNESS = 3

    def __init__(self, parent=None) -> None:
        super().__init__(parent)

        self._max_layers = 0
        self._current_layer_num = 0
        self._minimum_layer_num = 0
        self._current_layer_mesh = None
        self._current_layer_jumps = None
        self._top_layers_job = None  # type: Optional["_CreateTopLayersJob"]
        self._activity = False
        self._old_max_layers = 0

        self._max_paths = 0
        self._current_path_num = 0
        self._minimum_path_num = 0
        self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged)

        self._busy = False
        self._simulation_running = False

        self._ghost_shader = None  # type: Optional["ShaderProgram"]
        self._layer_pass = None  # type: Optional[SimulationPass]
        self._composite_pass = None  # type: Optional[CompositePass]
        self._old_layer_bindings = None  # type: Optional[List[str]]
        self._simulationview_composite_shader = None  # type: Optional["ShaderProgram"]
        self._old_composite_shader = None  # type: Optional["ShaderProgram"]

        self._max_feedrate = sys.float_info.min
        self._min_feedrate = sys.float_info.max
        self._max_thickness = sys.float_info.min
        self._min_thickness = sys.float_info.max

        self._global_container_stack = None  # type: Optional[ContainerStack]
        self._proxy = None

        self._resetSettings()
        self._legend_items = None
        self._show_travel_moves = False
        self._nozzle_node = None  # type: Optional[NozzleNode]

        Application.getInstance().getPreferences().addPreference(
            "view/top_layer_count", 5)
        Application.getInstance().getPreferences().addPreference(
            "view/only_show_top_layers", False)
        Application.getInstance().getPreferences().addPreference(
            "view/force_layer_view_compatibility_mode", False)

        Application.getInstance().getPreferences().addPreference(
            "layerview/layer_view_type", 0)
        Application.getInstance().getPreferences().addPreference(
            "layerview/extruder_opacities", "")

        Application.getInstance().getPreferences().addPreference(
            "layerview/show_travel_moves", False)
        Application.getInstance().getPreferences().addPreference(
            "layerview/show_helpers", True)
        Application.getInstance().getPreferences().addPreference(
            "layerview/show_skin", True)
        Application.getInstance().getPreferences().addPreference(
            "layerview/show_infill", True)

        self._updateWithPreferences()

        self._solid_layers = int(
            Application.getInstance().getPreferences().getValue(
                "view/top_layer_count"))
        self._only_show_top_layers = bool(
            Application.getInstance().getPreferences().getValue(
                "view/only_show_top_layers"))
        self._compatibility_mode = self._evaluateCompatibilityMode()

        self._wireprint_warning_message = Message(catalog.i18nc(
            "@info:status",
            "Cura does not accurately display layers when Wire Printing is enabled"
        ),
                                                  title=catalog.i18nc(
                                                      "@info:title",
                                                      "Simulation View"))

        QtApplication.getInstance().engineCreatedSignal.connect(
            self._onEngineCreated)

    def _onEngineCreated(self) -> None:
        plugin_path = PluginRegistry.getInstance().getPluginPath(
            self.getPluginId())
        if plugin_path:
            self.addDisplayComponent(
                "main",
                os.path.join(plugin_path, "SimulationViewMainComponent.qml"))
            self.addDisplayComponent(
                "menu",
                os.path.join(plugin_path, "SimulationViewMenuComponent.qml"))
        else:
            Logger.log("e", "Unable to find the path for %s",
                       self.getPluginId())

    def _evaluateCompatibilityMode(self) -> bool:
        return OpenGLContext.isLegacyOpenGL() or bool(
            Application.getInstance().getPreferences().getValue(
                "view/force_layer_view_compatibility_mode"))

    def _resetSettings(self) -> None:
        self._layer_view_type = 0  # type: int # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
        self._extruder_count = 0
        self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
        self._show_travel_moves = False
        self._show_helpers = True
        self._show_skin = True
        self._show_infill = True
        self.resetLayerData()

    def getActivity(self) -> bool:
        return self._activity

    def setActivity(self, activity: bool) -> None:
        if self._activity == activity:
            return
        self._activity = activity
        self.activityChanged.emit()

    def getSimulationPass(self) -> SimulationPass:
        if not self._layer_pass:
            # Currently the RenderPass constructor requires a size > 0
            # This should be fixed in RenderPass's constructor.
            self._layer_pass = SimulationPass(1, 1)
            self._compatibility_mode = self._evaluateCompatibilityMode()
            self._layer_pass.setSimulationView(self)
        return self._layer_pass

    def getCurrentLayer(self) -> int:
        return self._current_layer_num

    def getMinimumLayer(self) -> int:
        return self._minimum_layer_num

    def getMaxLayers(self) -> int:
        return self._max_layers

    def getCurrentPath(self) -> int:
        return self._current_path_num

    def getMinimumPath(self) -> int:
        return self._minimum_path_num

    def getMaxPaths(self) -> int:
        return self._max_paths

    def getNozzleNode(self) -> NozzleNode:
        if not self._nozzle_node:
            self._nozzle_node = NozzleNode()
        return self._nozzle_node

    def _onSceneChanged(self, node: "SceneNode") -> None:
        if node.getMeshData() is None:
            return
        self.setActivity(False)
        self.calculateMaxLayers()
        self.calculateMaxPathsOnLayer(self._current_layer_num)

    def isBusy(self) -> bool:
        return self._busy

    def setBusy(self, busy: bool) -> None:
        if busy != self._busy:
            self._busy = busy
            self.busyChanged.emit()

    def isSimulationRunning(self) -> bool:
        return self._simulation_running

    def setSimulationRunning(self, running: bool) -> None:
        self._simulation_running = running

    def resetLayerData(self) -> None:
        self._current_layer_mesh = None
        self._current_layer_jumps = None
        self._max_feedrate = sys.float_info.min
        self._min_feedrate = sys.float_info.max
        self._max_thickness = sys.float_info.min
        self._min_thickness = sys.float_info.max

    def beginRendering(self) -> None:
        scene = self.getController().getScene()
        renderer = self.getRenderer()
        if renderer is None:
            return

        if not self._ghost_shader:
            self._ghost_shader = OpenGL.getInstance().createShaderProgram(
                Resources.getPath(Resources.Shaders, "color.shader"))
            theme = CuraApplication.getInstance().getTheme()
            if theme is not None:
                self._ghost_shader.setUniformValue(
                    "u_color",
                    Color(*theme.getColor("layerview_ghost").getRgb()))

        for node in DepthFirstIterator(scene.getRoot()):
            # We do not want to render ConvexHullNode as it conflicts with the bottom layers.
            # However, it is somewhat relevant when the node is selected, so do render it then.
            if type(node) is ConvexHullNode and not Selection.isSelected(
                    cast(ConvexHullNode, node).getWatchedNode()):
                continue

            if not node.render(renderer):
                if (node.getMeshData()) and node.isVisible():
                    renderer.queueNode(node,
                                       transparent=True,
                                       shader=self._ghost_shader)

    def setLayer(self, value: int) -> None:
        if self._current_layer_num != value:
            self._current_layer_num = value
            if self._current_layer_num < 0:
                self._current_layer_num = 0
            if self._current_layer_num > self._max_layers:
                self._current_layer_num = self._max_layers
            if self._current_layer_num < self._minimum_layer_num:
                self._minimum_layer_num = self._current_layer_num

            self._startUpdateTopLayers()

            self.currentLayerNumChanged.emit()

    def setMinimumLayer(self, value: int) -> None:
        if self._minimum_layer_num != value:
            self._minimum_layer_num = value
            if self._minimum_layer_num < 0:
                self._minimum_layer_num = 0
            if self._minimum_layer_num > self._max_layers:
                self._minimum_layer_num = self._max_layers
            if self._minimum_layer_num > self._current_layer_num:
                self._current_layer_num = self._minimum_layer_num

            self._startUpdateTopLayers()

            self.currentLayerNumChanged.emit()

    def setPath(self, value: int) -> None:
        if self._current_path_num != value:
            self._current_path_num = value
            if self._current_path_num < 0:
                self._current_path_num = 0
            if self._current_path_num > self._max_paths:
                self._current_path_num = self._max_paths
            if self._current_path_num < self._minimum_path_num:
                self._minimum_path_num = self._current_path_num

            self._startUpdateTopLayers()

            self.currentPathNumChanged.emit()

    def setMinimumPath(self, value: int) -> None:
        if self._minimum_path_num != value:
            self._minimum_path_num = value
            if self._minimum_path_num < 0:
                self._minimum_path_num = 0
            if self._minimum_path_num > self._max_layers:
                self._minimum_path_num = self._max_layers
            if self._minimum_path_num > self._current_path_num:
                self._current_path_num = self._minimum_path_num

            self._startUpdateTopLayers()

            self.currentPathNumChanged.emit()

    ##  Set the layer view type
    #
    #   \param layer_view_type integer as in SimulationView.qml and this class
    def setSimulationViewType(self, layer_view_type: int) -> None:
        self._layer_view_type = layer_view_type
        self.currentLayerNumChanged.emit()

    ##  Return the layer view type, integer as in SimulationView.qml and this class
    def getSimulationViewType(self) -> int:
        return self._layer_view_type

    ##  Set the extruder opacity
    #
    #   \param extruder_nr 0..3
    #   \param opacity 0.0 .. 1.0
    def setExtruderOpacity(self, extruder_nr: int, opacity: float) -> None:
        if 0 <= extruder_nr <= 3:
            self._extruder_opacity[extruder_nr] = opacity
            self.currentLayerNumChanged.emit()

    def getExtruderOpacities(self) -> List[float]:
        return self._extruder_opacity

    def setShowTravelMoves(self, show):
        self._show_travel_moves = show
        self.currentLayerNumChanged.emit()

    def getShowTravelMoves(self):
        return self._show_travel_moves

    def setShowHelpers(self, show: bool) -> None:
        self._show_helpers = show
        self.currentLayerNumChanged.emit()

    def getShowHelpers(self) -> bool:
        return self._show_helpers

    def setShowSkin(self, show: bool) -> None:
        self._show_skin = show
        self.currentLayerNumChanged.emit()

    def getShowSkin(self) -> bool:
        return self._show_skin

    def setShowInfill(self, show: bool) -> None:
        self._show_infill = show
        self.currentLayerNumChanged.emit()

    def getShowInfill(self) -> bool:
        return self._show_infill

    def getCompatibilityMode(self) -> bool:
        return self._compatibility_mode

    def getExtruderCount(self) -> int:
        return self._extruder_count

    def getMinFeedrate(self) -> float:
        if abs(self._min_feedrate - sys.float_info.max
               ) < 10:  # Some lenience due to floating point rounding.
            return 0.0  # If it's still max-float, there are no measurements. Use 0 then.
        return self._min_feedrate

    def getMaxFeedrate(self) -> float:
        return self._max_feedrate

    def getMinThickness(self) -> float:
        if abs(self._min_thickness - sys.float_info.max
               ) < 10:  # Some lenience due to floating point rounding.
            return 0.0  # If it's still max-float, there are no measurements. Use 0 then.
        return self._min_thickness

    def getMaxThickness(self) -> float:
        return self._max_thickness

    def calculateMaxLayers(self) -> None:
        scene = self.getController().getScene()

        self._old_max_layers = self._max_layers
        ## Recalculate num max layers
        new_max_layers = -1
        for node in DepthFirstIterator(scene.getRoot()):  # type: ignore
            layer_data = node.callDecoration("getLayerData")
            if not layer_data:
                continue

            self.setActivity(True)
            min_layer_number = sys.maxsize
            max_layer_number = -sys.maxsize
            for layer_id in layer_data.getLayers():

                # If a layer doesn't contain any polygons, skip it (for infill meshes taller than print objects
                if len(layer_data.getLayer(layer_id).polygons) < 1:
                    continue

                # Store the max and min feedrates and thicknesses for display purposes
                for p in layer_data.getLayer(layer_id).polygons:
                    self._max_feedrate = max(float(p.lineFeedrates.max()),
                                             self._max_feedrate)
                    self._min_feedrate = min(float(p.lineFeedrates.min()),
                                             self._min_feedrate)
                    self._max_thickness = max(float(p.lineThicknesses.max()),
                                              self._max_thickness)
                    try:
                        self._min_thickness = min(
                            float(p.lineThicknesses[numpy.nonzero(
                                p.lineThicknesses)].min()),
                            self._min_thickness)
                    except ValueError:
                        # Sometimes, when importing a GCode the line thicknesses are zero and so the minimum (avoiding
                        # the zero) can't be calculated
                        Logger.log(
                            "i",
                            "Min thickness can't be calculated because all the values are zero"
                        )
                if max_layer_number < layer_id:
                    max_layer_number = layer_id
                if min_layer_number > layer_id:
                    min_layer_number = layer_id
            layer_count = max_layer_number - min_layer_number

            if new_max_layers < layer_count:
                new_max_layers = layer_count

        if new_max_layers >= 0 and new_max_layers != self._old_max_layers:
            self._max_layers = new_max_layers

            # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first
            # if it's the largest value. If we don't do this, we can have a slider block outside of the
            # slider.
            if new_max_layers > self._current_layer_num:
                self.maxLayersChanged.emit()
                self.setLayer(int(self._max_layers))
            else:
                self.setLayer(int(self._max_layers))
                self.maxLayersChanged.emit()
        self._startUpdateTopLayers()

    def calculateMaxPathsOnLayer(self, layer_num: int) -> None:
        # Update the currentPath
        scene = self.getController().getScene()
        for node in DepthFirstIterator(scene.getRoot()):  # type: ignore
            layer_data = node.callDecoration("getLayerData")
            if not layer_data:
                continue

            layer = layer_data.getLayer(layer_num)
            if layer is None:
                return
            new_max_paths = layer.lineMeshElementCount()
            if new_max_paths >= 0 and new_max_paths != self._max_paths:
                self._max_paths = new_max_paths
                self.maxPathsChanged.emit()

            self.setPath(int(new_max_paths))

    maxLayersChanged = Signal()
    maxPathsChanged = Signal()
    currentLayerNumChanged = Signal()
    currentPathNumChanged = Signal()
    globalStackChanged = Signal()
    preferencesChanged = Signal()
    busyChanged = Signal()
    activityChanged = Signal()

    ##  Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
    #   as this caused some issues.
    def getProxy(self, engine, script_engine):
        if self._proxy is None:
            self._proxy = SimulationViewProxy(self)
        return self._proxy

    def endRendering(self) -> None:
        pass

    def event(self, event) -> bool:
        modifiers = QApplication.keyboardModifiers()
        ctrl_is_active = modifiers & Qt.ControlModifier
        shift_is_active = modifiers & Qt.ShiftModifier
        if event.type == Event.KeyPressEvent and ctrl_is_active:
            amount = 10 if shift_is_active else 1
            if event.key == KeyEvent.UpKey:
                self.setLayer(self._current_layer_num + amount)
                return True
            if event.key == KeyEvent.DownKey:
                self.setLayer(self._current_layer_num - amount)
                return True

        if event.type == Event.ViewActivateEvent:
            # Start listening to changes.
            Application.getInstance().getPreferences(
            ).preferenceChanged.connect(self._onPreferencesChanged)
            self._controller.getScene().getRoot().childrenChanged.connect(
                self._onSceneChanged)

            self.calculateMaxLayers()
            self.calculateMaxPathsOnLayer(self._current_layer_num)

            # FIX: on Max OS X, somehow QOpenGLContext.currentContext() can become None during View switching.
            # This can happen when you do the following steps:
            #   1. Start Cura
            #   2. Load a model
            #   3. Switch to Custom mode
            #   4. Select the model and click on the per-object tool icon
            #   5. Switch view to Layer view or X-Ray
            #   6. Cura will very likely crash
            # It seems to be a timing issue that the currentContext can somehow be empty, but I have no clue why.
            # This fix tries to reschedule the view changing event call on the Qt thread again if the current OpenGL
            # context is None.
            if Platform.isOSX():
                if QOpenGLContext.currentContext() is None:
                    Logger.log(
                        "d",
                        "current context of OpenGL is empty on Mac OS X, will try to create shaders later"
                    )
                    CuraApplication.getInstance().callLater(
                        lambda e=event: self.event(e))
                    return False

            # Make sure the SimulationPass is created
            layer_pass = self.getSimulationPass()
            renderer = self.getRenderer()
            if renderer is None:
                return False

            renderer.addRenderPass(layer_pass)

            # Make sure the NozzleNode is add to the root
            nozzle = self.getNozzleNode()
            nozzle.setParent(self.getController().getScene().getRoot())
            nozzle.setVisible(False)

            Application.getInstance().globalContainerStackChanged.connect(
                self._onGlobalStackChanged)
            self._onGlobalStackChanged()

            if not self._simulationview_composite_shader:
                plugin_path = cast(
                    str,
                    PluginRegistry.getInstance().getPluginPath(
                        "SimulationView"))
                self._simulationview_composite_shader = OpenGL.getInstance(
                ).createShaderProgram(
                    os.path.join(plugin_path,
                                 "simulationview_composite.shader"))
                theme = CuraApplication.getInstance().getTheme()
                if theme is not None:
                    self._simulationview_composite_shader.setUniformValue(
                        "u_background_color",
                        Color(*theme.getColor("viewport_background").getRgb()))
                    self._simulationview_composite_shader.setUniformValue(
                        "u_outline_color",
                        Color(*theme.getColor(
                            "model_selection_outline").getRgb()))

            if not self._composite_pass:
                self._composite_pass = cast(
                    CompositePass, renderer.getRenderPass("composite"))

            self._old_layer_bindings = self._composite_pass.getLayerBindings(
            )[:]  # make a copy so we can restore to it later
            self._composite_pass.getLayerBindings().append("simulationview")
            self._old_composite_shader = self._composite_pass.getCompositeShader(
            )
            self._composite_pass.setCompositeShader(
                self._simulationview_composite_shader)

        elif event.type == Event.ViewDeactivateEvent:
            self._controller.getScene().getRoot().childrenChanged.disconnect(
                self._onSceneChanged)
            Application.getInstance().getPreferences(
            ).preferenceChanged.disconnect(self._onPreferencesChanged)
            self._wireprint_warning_message.hide()
            Application.getInstance().globalContainerStackChanged.disconnect(
                self._onGlobalStackChanged)
            if self._global_container_stack:
                self._global_container_stack.propertyChanged.disconnect(
                    self._onPropertyChanged)
            if self._nozzle_node:
                self._nozzle_node.setParent(None)

            renderer = self.getRenderer()
            if renderer is None:
                return False

            if self._layer_pass is not None:
                renderer.removeRenderPass(self._layer_pass)
            if self._composite_pass:
                self._composite_pass.setLayerBindings(
                    cast(List[str], self._old_layer_bindings))
                self._composite_pass.setCompositeShader(
                    cast(ShaderProgram, self._old_composite_shader))

        return False

    def getCurrentLayerMesh(self):
        return self._current_layer_mesh

    def getCurrentLayerJumps(self):
        return self._current_layer_jumps

    def _onGlobalStackChanged(self) -> None:
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.disconnect(
                self._onPropertyChanged)
        self._global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.connect(
                self._onPropertyChanged)
            self._extruder_count = self._global_container_stack.getProperty(
                "machine_extruder_count", "value")
            self._onPropertyChanged("wireframe_enabled", "value")
            self.globalStackChanged.emit()
        else:
            self._wireprint_warning_message.hide()

    def _onPropertyChanged(self, key: str, property_name: str) -> None:
        if key == "wireframe_enabled" and property_name == "value":
            if self._global_container_stack and self._global_container_stack.getProperty(
                    "wireframe_enabled", "value"):
                self._wireprint_warning_message.show()
            else:
                self._wireprint_warning_message.hide()

    def _onCurrentLayerNumChanged(self) -> None:
        self.calculateMaxPathsOnLayer(self._current_layer_num)

    def _startUpdateTopLayers(self) -> None:
        if not self._compatibility_mode:
            return

        if self._top_layers_job:
            self._top_layers_job.finished.disconnect(
                self._updateCurrentLayerMesh)
            self._top_layers_job.cancel()

        self.setBusy(True)

        self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(),
                                                   self._current_layer_num,
                                                   self._solid_layers)
        self._top_layers_job.finished.connect(
            self._updateCurrentLayerMesh
        )  # type: ignore  # mypy doesn't understand the whole private class thing that's going on here.
        self._top_layers_job.start()  # type: ignore

    def _updateCurrentLayerMesh(self, job: "_CreateTopLayersJob") -> None:
        self.setBusy(False)

        if not job.getResult():
            return
        self.resetLayerData(
        )  # Reset the layer data only when job is done. Doing it now prevents "blinking" data.
        self._current_layer_mesh = job.getResult().get("layers")
        if self._show_travel_moves:
            self._current_layer_jumps = job.getResult().get("jumps")
        self._controller.getScene().sceneChanged.emit(
            self._controller.getScene().getRoot())

        self._top_layers_job = None

    def _updateWithPreferences(self) -> None:
        self._solid_layers = int(
            Application.getInstance().getPreferences().getValue(
                "view/top_layer_count"))
        self._only_show_top_layers = bool(
            Application.getInstance().getPreferences().getValue(
                "view/only_show_top_layers"))
        self._compatibility_mode = self._evaluateCompatibilityMode()

        self.setSimulationViewType(
            int(
                float(Application.getInstance().getPreferences().getValue(
                    "layerview/layer_view_type"))))

        for extruder_nr, extruder_opacity in enumerate(
                Application.getInstance().getPreferences().getValue(
                    "layerview/extruder_opacities").split("|")):
            try:
                opacity = float(extruder_opacity)
            except ValueError:
                opacity = 1.0
            self.setExtruderOpacity(extruder_nr, opacity)

        self.setShowTravelMoves(
            bool(Application.getInstance().getPreferences().getValue(
                "layerview/show_travel_moves")))
        self.setShowHelpers(
            bool(Application.getInstance().getPreferences().getValue(
                "layerview/show_helpers")))
        self.setShowSkin(
            bool(Application.getInstance().getPreferences().getValue(
                "layerview/show_skin")))
        self.setShowInfill(
            bool(Application.getInstance().getPreferences().getValue(
                "layerview/show_infill")))

        self._startUpdateTopLayers()
        self.preferencesChanged.emit()

    def _onPreferencesChanged(self, preference: str) -> None:
        if preference not in {
                "view/top_layer_count",
                "view/only_show_top_layers",
                "view/force_layer_view_compatibility_mode",
                "layerview/layer_view_type",
                "layerview/extruder_opacities",
                "layerview/show_travel_moves",
                "layerview/show_helpers",
                "layerview/show_skin",
                "layerview/show_infill",
        }:
            return

        self._updateWithPreferences()
Exemple #50
0
class FlavorParser:
    def __init__(self) -> None:
        CuraApplication.getInstance().hideMessageSignal.connect(
            self._onHideMessage)
        self._cancelled = False
        self._message = None
        self._layer_number = 0
        self._extruder_number = 0
        self._clearValues()
        self._scene_node = None
        # X, Y, Z position, F feedrate and E extruder values are stored
        self._position = Position
        self._is_layers_in_file = False  # Does the Gcode have the layers comment?
        self._extruder_offsets = {
        }  # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
        self._current_layer_thickness = 0.2  # default
        self._filament_diameter = 2.85  # default

        CuraApplication.getInstance().getPreferences().addPreference(
            "gcodereader/show_caution", True)

    def _clearValues(self) -> None:
        self._extruder_number = 0
        self._extrusion_length_offset = [0]  # type: List[float]
        self._layer_type = LayerPolygon.Inset0Type
        self._layer_number = 0
        self._previous_z = 0  # type: float
        self._layer_data_builder = LayerDataBuilder()
        self._is_absolute_positioning = True  # It can be absolute (G90) or relative (G91)
        self._is_absolute_extrusion = True  # It can become absolute (M82, default) or relative (M83)

    @staticmethod
    def _getValue(line: str, code: str) -> Optional[Union[str, int, float]]:
        n = line.find(code)
        if n < 0:
            return None
        n += len(code)
        pattern = re.compile("[;\s]")
        match = pattern.search(line, n)
        m = match.start() if match is not None else -1
        try:
            if m < 0:
                return line[n:]
            return line[n:m]
        except:
            return None

    def _getInt(self, line: str, code: str) -> Optional[int]:
        value = self._getValue(line, code)
        try:
            return int(value)  # type: ignore
        except:
            return None

    def _getFloat(self, line: str, code: str) -> Optional[float]:
        value = self._getValue(line, code)
        try:
            return float(value)  # type: ignore
        except:
            return None

    def _onHideMessage(self, message: str) -> None:
        if message == self._message:
            self._cancelled = True

    def _createPolygon(self, layer_thickness: float,
                       path: List[List[Union[float, int]]],
                       extruder_offsets: List[float]) -> bool:
        countvalid = 0
        for point in path:
            if point[5] > 0:
                countvalid += 1
                if countvalid >= 2:
                    # we know what to do now, no need to count further
                    continue
        if countvalid < 2:
            return False
        try:
            self._layer_data_builder.addLayer(self._layer_number)
            self._layer_data_builder.setLayerHeight(self._layer_number,
                                                    path[0][2])
            self._layer_data_builder.setLayerThickness(self._layer_number,
                                                       layer_thickness)
            this_layer = self._layer_data_builder.getLayer(self._layer_number)
        except ValueError:
            return False
        count = len(path)
        line_types = numpy.empty((count - 1, 1), numpy.int32)
        line_widths = numpy.empty((count - 1, 1), numpy.float32)
        line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
        line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
        line_widths[:, 0] = 0.35  # Just a guess
        line_thicknesses[:, 0] = layer_thickness
        points = numpy.empty((count, 3), numpy.float32)
        extrusion_values = numpy.empty((count, 1), numpy.float32)
        i = 0
        for point in path:
            points[i, :] = [
                point[0] + extruder_offsets[0], point[2],
                -point[1] - extruder_offsets[1]
            ]
            extrusion_values[i] = point[4]
            if i > 0:
                line_feedrates[i - 1] = point[3]
                line_types[i - 1] = point[5]
                if point[5] in [
                        LayerPolygon.MoveCombingType,
                        LayerPolygon.MoveRetractionType
                ]:
                    line_widths[i - 1] = 0.1
                    line_thicknesses[
                        i - 1] = 0.0  # Travels are set as zero thickness lines
                else:
                    line_widths[i - 1] = self._calculateLineWidth(
                        points[i], points[i - 1], extrusion_values[i],
                        extrusion_values[i - 1], layer_thickness)
            i += 1

        this_poly = LayerPolygon(self._extruder_number, line_types, points,
                                 line_widths, line_thicknesses, line_feedrates)
        this_poly.buildCache()

        this_layer.polygons.append(this_poly)
        return True

    def _createEmptyLayer(self, layer_number: int) -> None:
        self._layer_data_builder.addLayer(layer_number)
        self._layer_data_builder.setLayerHeight(layer_number, 0)
        self._layer_data_builder.setLayerThickness(layer_number, 0)

    def _calculateLineWidth(self, current_point: Position,
                            previous_point: Position, current_extrusion: float,
                            previous_extrusion: float,
                            layer_thickness: float) -> float:
        # Area of the filament
        Af = (self._filament_diameter / 2)**2 * numpy.pi
        # Length of the extruded filament
        de = current_extrusion - previous_extrusion
        # Volumne of the extruded filament
        dVe = de * Af
        # Length of the printed line
        dX = numpy.sqrt((current_point[0] - previous_point[0])**2 +
                        (current_point[2] - previous_point[2])**2)
        # When the extruder recovers from a retraction, we get zero distance
        if dX == 0:
            return 0.1
        # Area of the printed line. This area is a rectangle
        Ae = dVe / dX
        # This area is a rectangle with area equal to layer_thickness * layer_width
        line_width = Ae / layer_thickness

        # A threshold is set to avoid weird paths in the GCode
        if line_width > 1.2:
            return 0.35
        return line_width

    def _gCode0(self, position: Position, params: PositionOptional,
                path: List[List[Union[float, int]]]) -> Position:
        x, y, z, f, e = position

        if self._is_absolute_positioning:
            x = params.x if params.x is not None else x
            y = params.y if params.y is not None else y
            z = params.z if params.z is not None else z
        else:
            x += params.x if params.x is not None else 0
            y += params.y if params.y is not None else 0
            z += params.z if params.z is not None else 0

        f = params.f if params.f is not None else f

        if params.e is not None:
            new_extrusion_value = params.e if self._is_absolute_extrusion else e[
                self._extruder_number] + params.e
            if new_extrusion_value > e[self._extruder_number]:
                path.append([
                    x, y, z, f, new_extrusion_value +
                    self._extrusion_length_offset[self._extruder_number],
                    self._layer_type
                ])  # extrusion
            else:
                path.append([
                    x, y, z, f, new_extrusion_value +
                    self._extrusion_length_offset[self._extruder_number],
                    LayerPolygon.MoveRetractionType
                ])  # retraction
            e[self._extruder_number] = new_extrusion_value

            # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
            # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
            if z > self._previous_z and (z - self._previous_z < 1.5):
                self._current_layer_thickness = z - self._previous_z  # allow a tiny overlap
                self._previous_z = z
        else:
            path.append([
                x, y, z, f, e[self._extruder_number] +
                self._extrusion_length_offset[self._extruder_number],
                LayerPolygon.MoveCombingType
            ])
        return self._position(x, y, z, f, e)

    # G0 and G1 should be handled exactly the same.
    _gCode1 = _gCode0

    ##  Home the head.
    def _gCode28(self, position: Position, params: PositionOptional,
                 path: List[List[Union[float, int]]]) -> Position:
        return self._position(params.x if params.x is not None else position.x,
                              params.y if params.y is not None else position.y,
                              params.z if params.z is not None else position.z,
                              position.f, position.e)

    ##  Set the absolute positioning
    def _gCode90(self, position: Position, params: PositionOptional,
                 path: List[List[Union[float, int]]]) -> Position:
        self._is_absolute_positioning = True
        self._is_absolute_extrusion = True
        return position

    ##  Set the relative positioning
    def _gCode91(self, position: Position, params: PositionOptional,
                 path: List[List[Union[float, int]]]) -> Position:
        self._is_absolute_positioning = False
        self._is_absolute_extrusion = False
        return position

    ##  Reset the current position to the values specified.
    #   For example: G92 X10 will set the X to 10 without any physical motion.
    def _gCode92(self, position: Position, params: PositionOptional,
                 path: List[List[Union[float, int]]]) -> Position:
        if params.e is not None:
            # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
            self._extrusion_length_offset[self._extruder_number] += position.e[
                self._extruder_number] - params.e
            position.e[self._extruder_number] = params.e
        return self._position(params.x if params.x is not None else position.x,
                              params.y if params.y is not None else position.y,
                              params.z if params.z is not None else position.z,
                              params.f if params.f is not None else position.f,
                              position.e)

    def processGCode(self, G: int, line: str, position: Position,
                     path: List[List[Union[float, int]]]) -> Position:
        func = getattr(self, "_gCode%s" % G, None)
        line = line.split(";", 1)[0]  # Remove comments (if any)
        if func is not None:
            s = line.upper().split(" ")
            x, y, z, f, e = None, None, None, None, None
            for item in s[1:]:
                if len(item) <= 1:
                    continue
                if item.startswith(";"):
                    continue
                if item[0] == "X":
                    x = float(item[1:])
                if item[0] == "Y":
                    y = float(item[1:])
                if item[0] == "Z":
                    z = float(item[1:])
                if item[0] == "F":
                    f = float(item[1:]) / 60
                if item[0] == "E":
                    e = float(item[1:])
            params = PositionOptional(x, y, z, f, e)
            return func(position, params, path)
        return position

    def processTCode(self, T: int, line: str, position: Position,
                     path: List[List[Union[float, int]]]) -> Position:
        self._extruder_number = T
        if self._extruder_number + 1 > len(position.e):
            self._extrusion_length_offset.extend(
                [0] * (self._extruder_number - len(position.e) + 1))
            position.e.extend([0] *
                              (self._extruder_number - len(position.e) + 1))
        return position

    def processMCode(self, M: int, line: str, position: Position,
                     path: List[List[Union[float, int]]]) -> Position:
        pass

    _type_keyword = ";TYPE:"
    _layer_keyword = ";LAYER:"

    ##  For showing correct x, y offsets for each extruder
    def _extruderOffsets(self) -> Dict[int, List[float]]:
        result = {}
        for extruder in ExtruderManager.getInstance().getExtruderStacks():
            result[int(extruder.getMetaData().get("position", "0"))] = [
                extruder.getProperty("machine_nozzle_offset_x", "value"),
                extruder.getProperty("machine_nozzle_offset_y", "value")
            ]
        return result

    def processGCodeStream(self, stream: str) -> Optional[CuraSceneNode]:
        Logger.log("d", "Preparing to load GCode")
        self._cancelled = False
        # We obtain the filament diameter from the selected extruder to calculate line widths
        global_stack = CuraApplication.getInstance().getGlobalContainerStack()

        if not global_stack:
            return None

        self._filament_diameter = global_stack.extruders[str(
            self._extruder_number)].getProperty("material_diameter", "value")

        scene_node = CuraSceneNode()

        gcode_list = []
        self._is_layers_in_file = False

        self._extruder_offsets = self._extruderOffsets(
        )  # dict with index the extruder number. can be empty

        ##############################################################################################
        ##  This part is where the action starts
        ##############################################################################################
        file_lines = 0
        current_line = 0
        for line in stream.split("\n"):
            file_lines += 1
            gcode_list.append(line + "\n")
            if not self._is_layers_in_file and line[:len(
                    self._layer_keyword)] == self._layer_keyword:
                self._is_layers_in_file = True

        file_step = max(math.floor(file_lines / 100), 1)

        self._clearValues()

        self._message = Message(catalog.i18nc("@info:status",
                                              "Parsing G-code"),
                                lifetime=0,
                                title=catalog.i18nc("@info:title",
                                                    "G-code Details"))

        assert (self._message is not None)  # use for typing purposes
        self._message.setProgress(0)
        self._message.show()

        Logger.log("d", "Parsing Gcode...")

        current_position = Position(0, 0, 0, 0, [0])
        current_path = []  #type: List[List[float]]
        min_layer_number = 0
        negative_layers = 0
        previous_layer = 0

        for line in stream.split("\n"):
            if self._cancelled:
                Logger.log("d", "Parsing Gcode file cancelled")
                return None
            current_line += 1

            if current_line % file_step == 0:
                self._message.setProgress(
                    math.floor(current_line / file_lines * 100))
                Job.yieldThread()
            if len(line) == 0:
                continue

            if line.find(self._type_keyword) == 0:
                type = line[len(self._type_keyword):].strip()
                if type == "WALL-INNER":
                    self._layer_type = LayerPolygon.InsetXType
                elif type == "WALL-OUTER":
                    self._layer_type = LayerPolygon.Inset0Type
                elif type == "SKIN":
                    self._layer_type = LayerPolygon.SkinType
                elif type == "SKIRT":
                    self._layer_type = LayerPolygon.SkirtType
                elif type == "SUPPORT":
                    self._layer_type = LayerPolygon.SupportType
                elif type == "FILL":
                    self._layer_type = LayerPolygon.InfillType
                else:
                    Logger.log(
                        "w",
                        "Encountered a unknown type (%s) while parsing g-code.",
                        type)

            # When the layer change is reached, the polygon is computed so we have just one layer per extruder
            if self._is_layers_in_file and line[:len(self._layer_keyword
                                                     )] == self._layer_keyword:
                try:
                    layer_number = int(line[len(self._layer_keyword):])
                    self._createPolygon(
                        self._current_layer_thickness, current_path,
                        self._extruder_offsets.get(self._extruder_number,
                                                   [0, 0]))
                    current_path.clear()
                    # Start the new layer at the end position of the last layer
                    current_path.append([
                        current_position.x, current_position.y,
                        current_position.z, current_position.f,
                        current_position.e[self._extruder_number],
                        LayerPolygon.MoveCombingType
                    ])

                    # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
                    # as in ProcessSlicedLayersJob
                    if layer_number < min_layer_number:
                        min_layer_number = layer_number
                    if layer_number < 0:
                        layer_number += abs(min_layer_number)
                        negative_layers += 1
                    else:
                        layer_number += negative_layers

                    # In case there is a gap in the layer count, empty layers are created
                    for empty_layer in range(previous_layer + 1, layer_number):
                        self._createEmptyLayer(empty_layer)

                    self._layer_number = layer_number
                    previous_layer = layer_number
                except:
                    pass

            # This line is a comment. Ignore it (except for the layer_keyword)
            if line.startswith(";"):
                continue

            G = self._getInt(line, "G")
            if G is not None:
                # When find a movement, the new posistion is calculated and added to the current_path, but
                # don't need to create a polygon until the end of the layer
                current_position = self.processGCode(G, line, current_position,
                                                     current_path)
                continue

            # When changing the extruder, the polygon with the stored paths is computed
            if line.startswith("T"):
                T = self._getInt(line, "T")
                if T is not None:
                    self._createPolygon(
                        self._current_layer_thickness, current_path,
                        self._extruder_offsets.get(self._extruder_number,
                                                   [0, 0]))
                    current_path.clear()

                    # When changing tool, store the end point of the previous path, then process the code and finally
                    # add another point with the new position of the head.
                    current_path.append([
                        current_position.x, current_position.y,
                        current_position.z, current_position.f,
                        current_position.e[self._extruder_number],
                        LayerPolygon.MoveCombingType
                    ])
                    current_position = self.processTCode(
                        T, line, current_position, current_path)
                    current_path.append([
                        current_position.x, current_position.y,
                        current_position.z, current_position.f,
                        current_position.e[self._extruder_number],
                        LayerPolygon.MoveCombingType
                    ])

            if line.startswith("M"):
                M = self._getInt(line, "M")
                self.processMCode(M, line, current_position, current_path)

        # "Flush" leftovers. Last layer paths are still stored
        if len(current_path) > 1:
            if self._createPolygon(
                    self._current_layer_thickness, current_path,
                    self._extruder_offsets.get(self._extruder_number, [0, 0])):
                self._layer_number += 1
                current_path.clear()

        material_color_map = numpy.zeros((8, 4), dtype=numpy.float32)
        material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
        material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
        material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
        material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
        material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
        material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
        material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
        material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
        layer_mesh = self._layer_data_builder.build(material_color_map)
        decorator = LayerDataDecorator()
        decorator.setLayerData(layer_mesh)
        scene_node.addDecorator(decorator)

        gcode_list_decorator = GCodeListDecorator()
        gcode_list_decorator.setGCodeList(gcode_list)
        scene_node.addDecorator(gcode_list_decorator)

        # gcode_dict stores gcode_lists for a number of build plates.
        active_build_plate_id = CuraApplication.getInstance(
        ).getMultiBuildPlateModel().activeBuildPlate
        gcode_dict = {active_build_plate_id: gcode_list}
        CuraApplication.getInstance().getController().getScene(
        ).gcode_dict = gcode_dict  #type: ignore #Because gcode_dict is generated dynamically.

        Logger.log("d", "Finished parsing Gcode")
        self._message.hide()

        if self._layer_number == 0:
            Logger.log("w", "File doesn't contain any valid layers")

        settings = CuraApplication.getInstance().getGlobalContainerStack()
        if not settings.getProperty("machine_center_is_zero", "value"):
            machine_width = settings.getProperty("machine_width", "value")
            machine_depth = settings.getProperty("machine_depth", "value")
            scene_node.setPosition(
                Vector(-machine_width / 2, 0, machine_depth / 2))

        Logger.log("d", "GCode loading finished")

        if CuraApplication.getInstance().getPreferences().getValue(
                "gcodereader/show_caution"):
            caution_message = Message(catalog.i18nc(
                "@info:generic",
                "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."
            ),
                                      lifetime=0,
                                      title=catalog.i18nc(
                                          "@info:title", "G-code Details"))
            caution_message.show()

        # The "save/print" button's state is bound to the backend state.
        backend = CuraApplication.getInstance().getBackend()
        backend.backendStateChange.emit(Backend.BackendState.Disabled)

        return scene_node
    def requestWrite(self,
                     nodes,
                     file_name=None,
                     limit_mimetypes=None,
                     file_handler=None,
                     **kwargs):
        application = cast(CuraApplication, Application.getInstance())
        machine_manager = application.getMachineManager()
        global_stack = machine_manager.activeMachine

        filename_format = Application.getInstance().getPreferences().getValue(
            "gcode_filename_format/filename_format")

        Logger.log("d", "filename_format = %s", filename_format)

        if filename_format is "":
            filename_format = DEFAULT_FILENAME_FORMAT

        if self._writing:
            raise OutputDeviceError.DeviceBusyError()

        self.getModifiedPrintSettings(application, global_stack)

        dialog = QFileDialog()

        dialog.setWindowTitle(catalog.i18nc("@title:window", "Save to File"))
        dialog.setFileMode(QFileDialog.AnyFile)
        dialog.setAcceptMode(QFileDialog.AcceptSave)

        dialog.setOption(QFileDialog.DontConfirmOverwrite)

        if sys.platform == "linux" and "KDE_FULL_SESSION" in os.environ:
            dialog.setOption(QFileDialog.DontUseNativeDialog)

        filters = []
        mime_types = []
        selected_filter = None

        if "preferred_mimetypes" in kwargs and kwargs[
                "preferred_mimetypes"] is not None:
            preferred_mimetypes = kwargs["preferred_mimetypes"]
        else:
            preferred_mimetypes = Application.getInstance().getPreferences(
            ).getValue("gcode_filename_format/last_used_type")
        preferred_mimetype_list = preferred_mimetypes.split(";")

        if not file_handler:
            file_handler = Application.getInstance().getMeshFileHandler()

        file_types = file_handler.getSupportedFileTypesWrite()

        file_types.sort(key=lambda k: k["description"])
        if limit_mimetypes:
            file_types = list(
                filter(lambda i: i["mime_type"] in limit_mimetypes,
                       file_types))

        file_types = [ft for ft in file_types if not ft["hide_in_file_dialog"]]

        if len(file_types) == 0:
            Logger.log("e", "There are no file types available to write with!")
            raise OutputDeviceError.WriteRequestFailedError(
                catalog.i18nc(
                    "@info:warning",
                    "There are no file types available to write with!"))

        preferred_mimetype = None
        for mime_type in preferred_mimetype_list:
            if any(ft["mime_type"] == mime_type for ft in file_types):
                preferred_mimetype = mime_type
                break

        for item in file_types:
            type_filter = "{0} (*.{1})".format(item["description"],
                                               item["extension"])
            filters.append(type_filter)
            mime_types.append(item["mime_type"])
            if preferred_mimetype == item["mime_type"]:
                selected_filter = type_filter
                file_name = self.parseFilenameFormat(filename_format,
                                                     file_name, application,
                                                     global_stack)
                #file_name += self.filenameTackOn(print_setting)
                if file_name:
                    file_name += "." + item["extension"]

        stored_directory = Application.getInstance().getPreferences().getValue(
            "gcode_filename_format/dialog_save_path")
        dialog.setDirectory(stored_directory)

        if file_name is not None:
            dialog.selectFile(file_name)

        dialog.setNameFilters(filters)
        if selected_filter is not None:
            dialog.selectNameFilter(selected_filter)

        if not dialog.exec_():
            raise OutputDeviceError.UserCanceledError()

        save_path = dialog.directory().absolutePath()
        Application.getInstance().getPreferences().setValue(
            "gcode_filename_format/dialog_save_path", save_path)

        selected_type = file_types[filters.index(dialog.selectedNameFilter())]
        Application.getInstance().getPreferences().setValue(
            "gcode_filename_format/last_used_type", selected_type["mime_type"])

        file_name = dialog.selectedFiles()[0]
        Logger.log("d", "Writing to [%s]..." % file_name)

        if os.path.exists(file_name):
            result = QMessageBox.question(
                None, catalog.i18nc("@title:window", "File Already Exists"),
                catalog.i18nc(
                    "@label Don't translate the XML tag <filename>!",
                    "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?"
                ).format(file_name))
            if result == QMessageBox.No:
                raise OutputDeviceError.UserCanceledError()

        self.writeStarted.emit(self)

        if file_handler:
            file_writer = file_handler.getWriter(selected_type["id"])
        else:
            file_writer = Application.getInstance().getMeshFileHandler(
            ).getWriter(selected_type["id"])

        try:
            mode = selected_type["mode"]
            if mode == MeshWriter.OutputMode.TextMode:
                Logger.log("d", "Writing to Local File %s in text mode",
                           file_name)
                stream = open(file_name, "wt", encoding="utf-8")
            elif mode == MeshWriter.OutputMode.BinaryMode:
                Logger.log("d", "Writing to Local File %s in binary mode",
                           file_name)
                stream = open(file_name, "wb")
            else:
                Logger.log("e", "Unrecognised OutputMode.")
                return None

            job = WriteFileJob(file_writer, stream, nodes, mode)
            job.setFileName(file_name)
            job.setAddToRecentFiles(True)
            job.progress.connect(self._onJobProgress)
            job.finished.connect(self._onWriteJobFinished)

            message = Message(
                catalog.i18nc(
                    "@info:progress Don't translate the XML tags <filename>!",
                    "Saving to <filename>{0}</filename>").format(file_name), 0,
                False, -1, catalog.i18nc("@info:title", "Saving"))
            message.show()

            job.setMessage(message)
            self._writing = True
            job.start()
        except PermissionError as e:
            Logger.log("e", "Permission denied when trying to write to %s: %s",
                       file_name, str(e))
            raise OutputDeviceError.PermissionDeniedError(
                catalog.i18nc(
                    "@info:status Don't translate the XML tags <filename>!",
                    "Permission denied when trying to save <filename>{0}</filename>"
                ).format(file_name)) from e
        except OSError as e:
            Logger.log("e",
                       "Operating system would not let us write to %s: %s",
                       file_name, str(e))
            raise OutputDeviceError.WriteRequestFailedError(
                catalog.i18nc(
                    "@info:status Don't translate the XML tags <filename> or <message>!",
                    "Could not save to <filename>{0}</filename>: <message>{1}</message>"
                ).format()) from e
Exemple #52
0
class CalibrationShapes(QObject, Extension):
    #Create an api
    from cura.CuraApplication import CuraApplication
    api = CuraApplication.getInstance().getCuraAPI()
    
    # The QT signal, which signals an update for user information text
    userInfoTextChanged = pyqtSignal()
    userSizeChanged = pyqtSignal()
    
    def __init__(self, parent = None) -> None:
        QObject.__init__(self, parent)
        Extension.__init__(self)
        
        #Inzialize varables
        self.userText = ""
        self._continueDialog = None
        
        # set the preferences to store the default value
        self._preferences = CuraApplication.getInstance().getPreferences()
        self._preferences.addPreference("calibrationshapes/size", 20)
        
        # convert as float to avoid further issue
        self._size = float(self._preferences.getValue("calibrationshapes/size"))
        
        # Suggested solution from fieldOfView . Unfortunatly it doesn't works 
        # https://github.com/5axes/Calibration-Shapes/issues/1
        # Cura should be able to find the scripts from inside the plugin folder if the scripts are into a folder named resources
        Resources.addSearchPath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources"))
 
        self.Major=1
        self.Minor=0

        # Logger.log('d', "Info Version CuraVersion --> " + str(Version(CuraVersion)))
        Logger.log('d', "Info CuraVersion --> " + str(CuraVersion))
        
        # Test version for futur release 4.9
        if "master" in CuraVersion or "beta" in CuraVersion or "BETA" in CuraVersion:
            # Master is always a developement version.
            self.Major=4
            self.Minor=9

        else:
            try:
                self.Major = int(CuraVersion.split(".")[0])
                self.Minor = int(CuraVersion.split(".")[1])
            except:
                pass
                
        self._controller = CuraApplication.getInstance().getController()
        self._message = None
        
        self.setMenuName(catalog.i18nc("@item:inmenu", "Part for calibration"))
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a cube"), self.addCube)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a cylinder"), self.addCylinder)
        # self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a sphere"), self.addSphere)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a tube"), self.addTube)
        self.addMenuItem("", lambda: None)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Calibration Cube"), self.addCalibrationCube)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a PLA TempTower"), self.addPLATempTower)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a PLA TempTower 190°C"), self.addPLATempTowerSimple)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a PLA+ TempTower"), self.addPLAPlusTempTower)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a PETG TempTower"), self.addPETGTempTower)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add an ABS TempTower"), self.addABSTempTower)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Retract Tower"), self.addRetractTower)
        
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Retract Test"), self.addRetractTest)
        # self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Junction Deviation Tower"), self.addJunctionDeviationTower)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Bridge Test"), self.addBridgeTest)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Thin Wall Test"), self.addThinWall)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add an Overhang Test"), self.addOverhangTest)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Flow Test"), self.addFlowTest)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Hole Test"), self.addHoleTest)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Tolerance Test"), self.addTolerance)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Support Test"), self.addSupportTest)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a MultiCube Calibration"), self.addMultiCube)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Bed Level Calibration"), self.addBedLevelCalibration)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Linear/Pressure Adv Tower"), self.addPressureAdvTower)
        self.addMenuItem("  ", lambda: None)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Cube bi-color"), self.addCubeBiColor)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add a Bi-Color Calibration Cube"), self.addHollowCalibrationCube)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Add an Extruder Offset Calibration Part"), self.addExtruderOffsetCalibration)        
        self.addMenuItem("   ", lambda: None)
        if self.Major < 4 or ( self.Major == 4 and self.Minor < 9 ) :
            self.addMenuItem(catalog.i18nc("@item:inmenu", "Copy Scripts"), self.copyScript)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Define default size"), self.defaultSize)
        self.addMenuItem("    ", lambda: None)
        self.addMenuItem(catalog.i18nc("@item:inmenu", "Help"), self.gotoHelp)
  
        #Inzialize varables
        self.userText = ""
        self._continueDialog = None
        
    # Define the default value pour the standard element
    def defaultSize(self) -> None:
    
        if self._continueDialog is None:
            self._continueDialog = self._createDialogue()
        self._continueDialog.show()
        #self.userSizeChanged.emit()
        
 
    #====User Input=====================================================================================================
    @pyqtProperty(str, notify= userSizeChanged)
    def sizeInput(self):
        return str(self._size)
        
    #The QT property, which is computed on demand from our userInfoText when the appropriate signal is emitted
    @pyqtProperty(str, notify= userInfoTextChanged)
    def userInfoText(self):
        return self.userText

    #This method builds the dialog from the qml file and registers this class
    #as the manager variable
    def _createDialogue(self):
        qml_file_path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "CalibrationShapes.qml")
        component_with_context = Application.getInstance().createQmlComponent(qml_file_path, {"manager": self})
        return component_with_context

    def getSize(self) -> float:
    
        return self._size
        
    # is called when a key gets released in the size inputField (twice for some reason)
    @pyqtSlot(str)
    def sizeEntered(self, text):
        # Is the textfield empty ? Don't show a message then
        if text =="":
            #self.writeToLog("size-Textfield: Empty")
            self.userMessage("", "ok")
            return

        #Convert commas to points
        text = text.replace(",",".")

        #self.writeToLog("Size-Textfield: read value "+text)

        #Is the entered Text a number?
        try:
            float(text)
        except ValueError:
            self.userMessage("Entered size invalid: " + text,"wrong")
            return
        self._size = float(text)

        #Check if positive
        if self._size <= 0:
            self.userMessage("Size value must be positive !","wrong")
            self._size = 20
            return

        self.writeToLog("Set calibrationshapes/size printFromHeight to : " + text)
        self._preferences.setValue("calibrationshapes/size", self._size)
        
        #clear the message Field
        self.userMessage("", "ok")
 
    #===== Text Output ===================================================================================================
    #writes the message to the log, includes timestamp, length is fixed
    def writeToLog(self, str):
        Logger.log("d", "Debug calibration shapes = %s", str)

    #Sends an user message to the Info Textfield, color depends on status (prioritized feedback)
    # Red wrong for Errors and Warnings
    # Grey for details and messages that aren't interesting for advanced users
    def userMessage(self, message, status):
        if status is "wrong":
            #Red
            self.userText = "<font color='#a00000'>" + message + "</font>"
        else:
            # Grey
            if status is "ok":
                self.userText = "<font color='#9fa4b0'>" + message + "</font>"
            else:
                self.writeToLog("Error: Invalid status: "+status)
                return
        #self.writeToLog("User Message: "+message)
        self.userInfoTextChanged.emit()
 
    # Copy the scripts to the right directory ( Temporary solution)
    def copyScript(self) -> None:
        File_List = ['RetractTower.py', 'SpeedTower.py', 'TempFanTower.py']
        
        plugPath =  os.path.join(os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources"), "scripts")
        # Logger.log("d", "plugPath= %s", plugPath)
        
        stringMatch = re.split("plugins", plugPath)
        destPath = os.path.join(stringMatch[0], "scripts")
        nbfile=0
        # Copy the script
        for fl in File_List:
            script_definition_path = os.path.join(plugPath, fl)
            dest_definition_path = os.path.join(destPath, fl)
            self.writeToLog("Dest_definition_path = " + dest_definition_path)
            if os.path.isfile(dest_definition_path)==True:
                self.writeToLog("Script_definition_path st_size= " + str(os.stat(script_definition_path).st_size))
                self.writeToLog("Dest_definition_path st_size= " + str(os.stat(dest_definition_path).st_size))
            
            if os.path.isfile(dest_definition_path)==False:
                self.writeToLog("Copy Script_definition_path = " + script_definition_path)
                copyfile(script_definition_path,dest_definition_path)
                nbfile+=1
            # Change condition to File Size definition
            elif os.stat(script_definition_path).st_size > os.stat(dest_definition_path).st_size :
                self.writeToLog("Copy Script_definition_path = " + script_definition_path)
                copyfile(script_definition_path,dest_definition_path)
                nbfile+=1
        
        txt_Message = ""
        if nbfile > 0 :
            txt_Message =  str(nbfile) + " script(s) copied in :\n"
            txt_Message = txt_Message + destPath
            txt_Message = txt_Message + "\nYou must now restart Cura to see the scripts in the postprocessing script list"
        else:
            txt_Message = "Every script are up to date in :\n"
            txt_Message = txt_Message + destPath
          
        self._message = Message(catalog.i18nc("@info:status", txt_Message), title = catalog.i18nc("@title", "Calibration Shapes"))
        self._message.show()
     
    def gotoHelp(self) -> None:
        QDesktopServices.openUrl(QUrl("https://github.com/5axes/Calibration-Shapes/wiki"))

    def addBedLevelCalibration(self) -> None:
        # Get the build plate Size
        machine_manager = CuraApplication.getInstance().getMachineManager()        
        stack = CuraApplication.getInstance().getGlobalContainerStack()

        global_stack = machine_manager.activeMachine
        m_w=global_stack.getProperty("machine_width", "value") 
        m_d=global_stack.getProperty("machine_depth", "value")
        if (m_w/m_d)>1.15 or (m_d/m_w)>1.15:
            factor_w=round((m_w/100), 1)
            factor_d=round((m_d/100), 1) 
        else:
            factor_w=int(m_w/100)
            factor_d=int(m_d/100)          
        
        # Logger.log("d", "factor_w= %.1f", factor_w)
        # Logger.log("d", "factor_d= %.1f", factor_d)
        
        model_definition_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models", "ParametricBedLevel.stl")
        mesh = trimesh.load(model_definition_path)
        origin = [0, 0, 0]
        DirX = [1, 0, 0]
        DirY = [0, 1, 0]
        # DirZ = [0, 0, 1]
        mesh.apply_transform(trimesh.transformations.scale_matrix(factor_w, origin, DirX))
        mesh.apply_transform(trimesh.transformations.scale_matrix(factor_d, origin, DirY))
        # addShape
        self._addShape("BedLevelCalibration",self._toMeshData(mesh))
        
            
    def _registerShapeStl(self, mesh_name, mesh_filename=None, **kwargs) -> None:
        if mesh_filename is None:
            mesh_filename = f"{mesh_name}.stl"
        
        model_definition_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models", mesh_filename)
        mesh =  trimesh.load(model_definition_path)
        # addShape
        self._addShape(mesh_name,self._toMeshData(mesh), **kwargs)
        
    def addCalibrationCube(self) -> None:
        self._registerShapeStl("CalibrationCube")

    def addMultiCube(self) -> None:
        self._registerShapeStl("MultiCube")

    def addJunctionDeviationTower(self) -> None:
        self._registerShapeStl("JunctionDeviationTower")
    
    def addPLATempTower(self) -> None:
        self._registerShapeStl("PLATempTower", "TempTowerPLA.stl")
        self._checkAdaptativ(False)

    def addPLATempTowerSimple(self) -> None:
        self._registerShapeStl("PLATempTower", "TempTowerPLA190°C.stl")
        self._checkAdaptativ(False)

    def addPLAPlusTempTower(self) -> None:
        self._registerShapeStl("PLA+TempTower", "TempTowerPLA+.stl")
        self._checkAdaptativ(False)
        
    def addPETGTempTower(self) -> None:
        self._registerShapeStl("PETGTempTower", "TempTowerPETG.stl")
        self._checkAdaptativ(False)
        
    def addABSTempTower(self) -> None:
        self._registerShapeStl("ABSTempTower", "TempTowerABS.stl")
        self._checkAdaptativ(False)

    def addRetractTower(self) -> None:
        self._registerShapeStl("RetractTower")
        self._checkAdaptativ(False)
        
    def addRetractTest(self) -> None:
        self._registerShapeStl("RetractTest")
        
    def addBridgeTest(self) -> None:
        self._registerShapeStl("BridgeTest")

    def addThinWall(self) -> None:
        self._registerShapeStl("ThinWall")
 
    def addOverhangTest(self) -> None:
        self._registerShapeStl("OverhangTest", "Overhang.stl")
 
    def addFlowTest(self) -> None:
        self._registerShapeStl("FlowTest", "FlowTest.stl")

    def addHoleTest(self) -> None:
        self._registerShapeStl("FlowTest", "HoleTest.stl")

    def addTolerance(self) -> None:
        self._registerShapeStl("Tolerance")

    # Dotdash addition 2 - Support test
    def addSupportTest(self) -> None:
        self._registerShapeStl("SupportTest")

    # Dotdash addition - for Linear/Pressure advance
    def addPressureAdvTower(self) -> None:
        self._registerShapeStl("PressureAdv", "PressureAdvTower.stl")

    # Dotdash addition 2 - Support test
    def addSupportTest(self) -> None:
        self._registerShapeStl("SupportTest")

    # Dotdash addition - for Linear/Pressure advance
    def addPressureAdvTower(self) -> None:
        model_definition_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models", "PressureAdvTower.stl")
        mesh =  trimesh.load(model_definition_path)
        # addShape
        self._addShape("PressureAdv",self._toMeshData(mesh))

    #-----------------------------
    #   Dual Extruder 
    #----------------------------- 
    def addCubeBiColor(self) -> None:
        self._registerShapeStl("CubeBiColorExt1", "CubeBiColorWhite.stl", ext_pos=1)
        self._registerShapeStl("CubeBiColorExt2", "CubeBiColorRed.stl", ext_pos=2)

    def addHollowCalibrationCube(self) -> None:
        self._registerShapeStl("CubeBiColorExt", "HollowCalibrationCube.stl", ext_pos=1)
        self._registerShapeStl("CubeBiColorInt", "HollowCenterCube.stl", ext_pos=2)
        
    def addExtruderOffsetCalibration(self) -> None:
        self._registerShapeStl("CalibrationMultiExtruder1", "nozzle-to-nozzle-xy-offset-calibration-pattern-a.stl", ext_pos=1)
        self._registerShapeStl("CalibrationMultiExtruder1", "nozzle-to-nozzle-xy-offset-calibration-pattern-b.stl", ext_pos=2)

    #-----------------------------
    #   Standard Geometry  
    #-----------------------------    
    # Origin, xaxis, yaxis, zaxis = [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]
    # S = trimesh.transformations.scale_matrix(20, origin)
    # xaxis = [1, 0, 0]
    # Rx = trimesh.transformations.rotation_matrix(math.radians(90), xaxis)    
    def addCube(self) -> None:
        Tz = trimesh.transformations.translation_matrix([0, 0, self._size*0.5])
        self._addShape("Cube",self._toMeshData(trimesh.creation.box(extents = [self._size, self._size, self._size], transform = Tz )))
        
    def addCylinder(self) -> None:
        mesh = trimesh.creation.cylinder(radius = self._size / 2, height = self._size, sections=90)
        mesh.apply_transform(trimesh.transformations.translation_matrix([0, 0, self._size*0.5]))
        self._addShape("Cylinder",self._toMeshData(mesh))

    def addTube(self) -> None:
        mesh = trimesh.creation.annulus(r_min = self._size / 4, r_max = self._size / 2, height = self._size, sections = 90)
        mesh.apply_transform(trimesh.transformations.translation_matrix([0, 0, self._size*0.5]))
        self._addShape("Tube",self._toMeshData(mesh))
        
    # Sphere are not very usefull but I leave it for the moment    
    def addSphere(self) -> None:
        # subdivisions (int) – How many times to subdivide the mesh. Note that the number of faces will grow as function of 4 ** subdivisions, so you probably want to keep this under ~5
        mesh = trimesh.creation.icosphere(subdivisions=4,radius = self._size / 2,)
        mesh.apply_transform(trimesh.transformations.translation_matrix([0, 0, self._size*0.5]))
        self._addShape("Sphere",self._toMeshData(mesh))

    #----------------------------------------------------------
    # Check adaptive_layer_height_enabled must be False
    #----------------------------------------------------------   
    def _checkAdaptativ(self, val):
        # Logger.log("d", "In checkAdaptativ = %s", str(val))
        # Fix some settings in Cura to get a better result
        global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() 
        adaptive_layer = global_container_stack.getProperty("adaptive_layer_height_enabled", "value")
        extruder = global_container_stack.extruderList[0]
        
        if adaptive_layer !=  val :
            Message(text = "Info modification current profil adaptive_layer_height_enabled\nNew value : %s" % (str(val)), title = catalog.i18nc("@info:title", "Warning ! Calibration Shapes")).show()
            # Define adaptive_layer
            global_container_stack.setProperty("adaptive_layer_height_enabled", "value", False)
        
        nozzle_size = float(extruder.getProperty("machine_nozzle_size", "value"))
        remove_holes = extruder.getProperty("meshfix_union_all_remove_holes", "value")
        # Logger.log("d", "In checkAdaptativ nozzle_size = %s", str(nozzle_size))
        # Logger.log("d", "In checkAdaptativ remove_holes = %s", str(remove_holes))
        
        if (nozzle_size >  0.4) and (remove_holes == False) :
            Message(text = "Info modification current profil meshfix_union_all_remove_holes (machine_nozzle_size>0.4)\nNew value : %s" % (str(True)), title = catalog.i18nc("@info:title", "Warning ! Calibration Shapes")).show()
            # Define adaptive_layer
            extruder.setProperty("meshfix_union_all_remove_holes", "value", True) 
            
    #----------------------------------------
    # Initial Source code from  fieldOfView
    #----------------------------------------  
    def _toMeshData(self, tri_node: trimesh.base.Trimesh) -> MeshData:
        # Rotate the part to laydown on the build plate
        # Modification from 5@xes
        tri_node.apply_transform(trimesh.transformations.rotation_matrix(math.radians(90), [-1, 0, 0]))
        tri_faces = tri_node.faces
        tri_vertices = tri_node.vertices

        # Following Source code from  fieldOfView
        # https://github.com/fieldOfView/Cura-SimpleShapes/blob/bac9133a2ddfbf1ca6a3c27aca1cfdd26e847221/SimpleShapes.py#L45
        indices = []
        vertices = []

        index_count = 0
        face_count = 0
        for tri_face in tri_faces:
            face = []
            for tri_index in tri_face:
                vertices.append(tri_vertices[tri_index])
                face.append(index_count)
                index_count += 1
            indices.append(face)
            face_count += 1

        vertices = numpy.asarray(vertices, dtype=numpy.float32)
        indices = numpy.asarray(indices, dtype=numpy.int32)
        normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count)

        mesh_data = MeshData(vertices=vertices, indices=indices, normals=normals)

        return mesh_data        
        
    # Initial Source code from  fieldOfView
    # https://github.com/fieldOfView/Cura-SimpleShapes/blob/bac9133a2ddfbf1ca6a3c27aca1cfdd26e847221/SimpleShapes.py#L70
    def _addShape(self, mesh_name, mesh_data: MeshData, ext_pos = 0 ) -> None:
        application = CuraApplication.getInstance()
        global_stack = application.getGlobalContainerStack()
        if not global_stack:
            return

        node = CuraSceneNode()

        node.setMeshData(mesh_data)
        node.setSelectable(True)
        if len(mesh_name)==0:
            node.setName("TestPart" + str(id(mesh_data)))
        else:
            node.setName(str(mesh_name))

        scene = self._controller.getScene()
        op = AddSceneNodeOperation(node, scene.getRoot())
        op.push()

        extruder_nr=len(global_stack.extruders)
        # Logger.log("d", "extruder_nr= %d", extruder_nr)
        # default_extruder_position  : <class 'str'>
        if ext_pos>0 and ext_pos<=extruder_nr :
            default_extruder_position = str(ext_pos-1)
        else :
            default_extruder_position = application.getMachineManager().defaultExtruderPosition
        # Logger.log("d", "default_extruder_position= %s", type(default_extruder_position))
        # default_extruder_id = global_stack.extruders[default_extruder_position].getId()
        default_extruder_id = global_stack.extruders[default_extruder_position].getId()
        # Logger.log("d", "default_extruder_id= %s", default_extruder_id)
        node.callDecoration("setActiveExtruder", default_extruder_id)

        active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
        node.addDecorator(BuildPlateDecorator(active_build_plate))

        node.addDecorator(SliceableObjectDecorator())

        application.getController().getScene().sceneChanged.emit(node)
Exemple #53
0
    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)
                    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._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):
                            if ("tool%d" % index) in json_data["temperature"]:
                                hotend_temperatures = json_data["temperature"]["tool%d" % index]
                                self._setHotendTemperature(index, hotend_temperatures["actual"])
                                self._updateTargetHotendTemperature(index, hotend_temperatures["target"])
                            else:
                                self._setHotendTemperature(index, 0)
                                self._updateTargetHotendTemperature(index, 0)

                        if "bed" in json_data["temperature"]:
                            bed_temperatures = json_data["temperature"]["bed"]
                            self._setBedTemperature(bed_temperatures["actual"])
                            self._updateTargetBedTemperature(bed_temperatures["target"])
                        else:
                            self._setBedTemperature(0)
                            self._updateTargetBedTemperature(0)

                    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:
                    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 = {}

                    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 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._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()

        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())
Exemple #54
0
class Account(QObject):
    """The account API provides a version-proof bridge to use Ultimaker Accounts

    Usage:

    .. code-block:: python

      from cura.API import CuraAPI
      api = CuraAPI()
      api.account.login()
      api.account.logout()
      api.account.userProfile    # Who is logged in
    """

    # The interval in which sync services are automatically triggered
    SYNC_INTERVAL = 30.0  # seconds
    Q_ENUMS(SyncState)

    loginStateChanged = pyqtSignal(bool)
    """Signal emitted when user logged in or out"""

    accessTokenChanged = pyqtSignal()
    syncRequested = pyqtSignal()
    """Sync services may connect to this signal to receive sync triggers.
    Services should be resilient to receiving a signal while they are still syncing,
    either by ignoring subsequent signals or restarting a sync.
    See setSyncState() for providing user feedback on the state of your service. 
    """
    lastSyncDateTimeChanged = pyqtSignal()
    syncStateChanged = pyqtSignal(int)  # because SyncState is an int Enum
    manualSyncEnabledChanged = pyqtSignal(bool)
    updatePackagesEnabledChanged = pyqtSignal(bool)

    def __init__(self, application: "CuraApplication", parent = None) -> None:
        super().__init__(parent)
        self._application = application
        self._new_cloud_printers_detected = False

        self._error_message = None  # type: Optional[Message]
        self._logged_in = False
        self._sync_state = SyncState.IDLE
        self._manual_sync_enabled = False
        self._update_packages_enabled = False
        self._update_packages_action = None  # type: Optional[Callable]
        self._last_sync_str = "-"

        self._callback_port = 32118
        self._oauth_root = UltimakerCloudConstants.CuraCloudAccountAPIRoot

        self._oauth_settings = OAuth2Settings(
            OAUTH_SERVER_URL= self._oauth_root,
            CALLBACK_PORT=self._callback_port,
            CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
            CLIENT_ID="um----------------------------ultimaker_cura",
            CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download "
                          "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write "
                          "library.project.read library.project.write cura.printjob.read cura.printjob.write "
                          "cura.mesh.read cura.mesh.write",
            AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
            AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
            AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
        )

        self._authorization_service = AuthorizationService(self._oauth_settings)

        # Create a timer for automatic account sync
        self._update_timer = QTimer()
        self._update_timer.setInterval(int(self.SYNC_INTERVAL * 1000))
        # The timer is restarted explicitly after an update was processed. This prevents 2 concurrent updates
        self._update_timer.setSingleShot(True)
        self._update_timer.timeout.connect(self.sync)

        self._sync_services = {}  # type: Dict[str, int]
        """contains entries "service_name" : SyncState"""

    def initialize(self) -> None:
        self._authorization_service.initialize(self._application.getPreferences())
        self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
        self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
        self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
        self._authorization_service.loadAuthDataFromPreferences()


    @pyqtProperty(int, notify=syncStateChanged)
    def syncState(self):
        return self._sync_state

    def setSyncState(self, service_name: str, state: int) -> None:
        """ Can be used to register sync services and update account sync states

        Contract: A sync service is expected exit syncing state in all cases, within reasonable time

        Example: `setSyncState("PluginSyncService", SyncState.SYNCING)`
        :param service_name: A unique name for your service, such as `plugins` or `backups`
        :param state: One of SyncState
        """
        prev_state = self._sync_state

        self._sync_services[service_name] = state

        if any(val == SyncState.SYNCING for val in self._sync_services.values()):
            self._sync_state = SyncState.SYNCING
            self._setManualSyncEnabled(False)
        elif any(val == SyncState.ERROR for val in self._sync_services.values()):
            self._sync_state = SyncState.ERROR
            self._setManualSyncEnabled(True)
        else:
            self._sync_state = SyncState.SUCCESS
            self._setManualSyncEnabled(False)

        if self._sync_state != prev_state:
            self.syncStateChanged.emit(self._sync_state)

            if self._sync_state == SyncState.SUCCESS:
                self._last_sync_str = datetime.now().strftime("%d/%m/%Y %H:%M")
                self.lastSyncDateTimeChanged.emit()

            if self._sync_state != SyncState.SYNCING:
                # schedule new auto update after syncing completed (for whatever reason)
                if not self._update_timer.isActive():
                    self._update_timer.start()

    def setUpdatePackagesAction(self, action: Callable) -> None:
        """ Set the callback which will be invoked when the user clicks the update packages button

        Should be invoked after your service sets the sync state to SYNCING and before setting the
        sync state to SUCCESS.

        Action will be reset to None when the next sync starts
        """
        self._update_packages_action = action
        self._update_packages_enabled = True
        self.updatePackagesEnabledChanged.emit(self._update_packages_enabled)

    def _onAccessTokenChanged(self):
        self.accessTokenChanged.emit()

    @property
    def is_staging(self) -> bool:
        """Indication whether the given authentication is applied against staging or not."""

        return "staging" in self._oauth_root

    @pyqtProperty(bool, notify=loginStateChanged)
    def isLoggedIn(self) -> bool:
        return self._logged_in

    def _onLoginStateChanged(self, logged_in: bool = False, error_message: Optional[str] = None) -> None:
        if error_message:
            if self._error_message:
                self._error_message.hide()
            self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
            self._error_message.show()
            self._logged_in = False
            self.loginStateChanged.emit(False)
            if self._update_timer.isActive():
                self._update_timer.stop()
            return

        if self._logged_in != logged_in:
            self._logged_in = logged_in
            self.loginStateChanged.emit(logged_in)
            if logged_in:
                self._setManualSyncEnabled(False)
                self._sync()
            else:
                if self._update_timer.isActive():
                    self._update_timer.stop()

    def _sync(self) -> None:
        """Signals all sync services to start syncing

        This can be considered a forced sync: even when a
        sync is currently running, a sync will be requested.
        """

        self._update_packages_action = None
        self._update_packages_enabled = False
        self.updatePackagesEnabledChanged.emit(self._update_packages_enabled)
        if self._update_timer.isActive():
            self._update_timer.stop()
        elif self._sync_state == SyncState.SYNCING:
            Logger.warning("Starting a new sync while previous sync was not completed\n{}", str(self._sync_services))

        self.syncRequested.emit()

    def _setManualSyncEnabled(self, enabled: bool) -> None:
        if self._manual_sync_enabled != enabled:
            self._manual_sync_enabled = enabled
            self.manualSyncEnabledChanged.emit(enabled)

    @pyqtSlot()
    @pyqtSlot(bool)
    def login(self, force_logout_before_login: bool = False) -> None:
        """
        Initializes the login process. If the user is logged in already and force_logout_before_login is true, Cura will
        logout from the account before initiating the authorization flow. If the user is logged in and
        force_logout_before_login is false, the function will return, as there is nothing to do.

        :param force_logout_before_login: Optional boolean parameter
        :return: None
        """
        if self._logged_in:
            if force_logout_before_login:
                self.logout()
            else:
                # Nothing to do, user already logged in.
                return
        self._authorization_service.startAuthorizationFlow(force_logout_before_login)

    @pyqtProperty(str, notify=loginStateChanged)
    def userName(self):
        user_profile = self._authorization_service.getUserProfile()
        if not user_profile:
            return None
        return user_profile.username

    @pyqtProperty(str, notify = loginStateChanged)
    def profileImageUrl(self):
        user_profile = self._authorization_service.getUserProfile()
        if not user_profile:
            return None
        return user_profile.profile_image_url

    @pyqtProperty(str, notify=accessTokenChanged)
    def accessToken(self) -> Optional[str]:
        return self._authorization_service.getAccessToken()

    @pyqtProperty("QVariantMap", notify = loginStateChanged)
    def userProfile(self) -> Optional[Dict[str, Optional[str]]]:
        """None if no user is logged in otherwise the logged in  user as a dict containing containing user_id, username and profile_image_url """

        user_profile = self._authorization_service.getUserProfile()
        if not user_profile:
            return None
        return user_profile.__dict__

    @pyqtProperty(str, notify=lastSyncDateTimeChanged)
    def lastSyncDateTime(self) -> str:
        return self._last_sync_str

    @pyqtProperty(bool, notify=manualSyncEnabledChanged)
    def manualSyncEnabled(self) -> bool:
        return self._manual_sync_enabled

    @pyqtProperty(bool, notify=updatePackagesEnabledChanged)
    def updatePackagesEnabled(self) -> bool:
        return self._update_packages_enabled

    @pyqtSlot()
    @pyqtSlot(bool)
    def sync(self, user_initiated: bool = False) -> None:
        if user_initiated:
            self._setManualSyncEnabled(False)

        self._sync()

    @pyqtSlot()
    def onUpdatePackagesClicked(self) -> None:
        if self._update_packages_action is not None:
            self._update_packages_action()

    @pyqtSlot()
    def popupOpened(self) -> None:
        self._setManualSyncEnabled(True)

    @pyqtSlot()
    def logout(self) -> None:
        if not self._logged_in:
            return  # Nothing to do, user isn't logged in.

        self._authorization_service.deleteAuthData()
    def handleWrite(self,
                    nodes,
                    file_name=None,
                    limit_mimetypes=None,
                    file_handler=None,
                    **kwargs):
        #Logger.log("d","In handleWrite")
        self._writeHandleTimer.setInterval(1000)
        result = None
        if not self._printer.state == SnapmakerApiV1.SnapmakerApiState.IDLE:
            if (self._printer.state ==
                    SnapmakerApiV1.SnapmakerApiState.NOTCONNECTED):
                result = self._printer.connect()
                if result == False:
                    self.writeError.emit()
                    self._connect_failed_message.show()
                    return
            elif (self._printer.state == SnapmakerApiV1.SnapmakerApiState.FATAL
                  ):
                #Logger.log("d",self._printer.state)
                self._printer = SnapmakerApiV1.SnapmakerApiV1(
                    self._uri, self._printer.token)
                result = self._printer.connect()
                if result == False:
                    self.writeError.emit()
                    self._connect_failed_message.show()
                    return
            elif (self._printer.state ==
                  SnapmakerApiV1.SnapmakerApiState.AWAITING_AUTHORIZATION):
                #Logger.log("d",self._printer.state)
                self._authrequired_message.show()
            else:
                #Logger.log("d",self._printer.state)
                self.writeError.emit()
                message = Message(i18n_catalog.i18nc(
                    "@message", "Sending failed, try again later"),
                                  lifetime=30,
                                  dismissable=True,
                                  title='Error')
                message.show()
                return

            self._writeHandleTimer.start()
            return
        #Logger.log("d","Ready to send")
        self._authrequired_message.hide()
        self._prepare_send_message.show()
        self._token = self._printer.token
        self.writeStarted.emit(self)
        print_info = CuraApplication.getInstance().getPrintInformation()
        gcode_writer = MeshWriter()
        self._gcode_stream = StringIO()
        #In case the Plugin Gcodewriter is a separate Plugin
        #try:
        #gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("CuraSnapmakerSender"))
        #except UM.PluginError.PluginNotFoundError:
        #gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
        gcode_writer = SnapmakerGCodeWriter.SnapmakerGCodeWriter()
        if not gcode_writer.write(self._gcode_stream, None):
            #Logger.log("e", "GCodeWrite failed: %s" % gcode_writer.getInformation())
            return
        self.content_length = self._gcode_stream.tell()
        self._gcode_stream.seek(0)
        self._byteStream = BytesIOWrapper(self._gcode_stream)
        self._printer.setBlocking(False)
        self.active_sending_future = self._printer.send_gcode_file(
            print_info.jobName.strip() + ".gcode",
            self._byteStream,
            callback=self.updateProgress)
        self.active_sending_future.add_done_callback(self.transmitDone)
        self._printer.setBlocking(True)
        self._progress_message.setMaxProgress(100)
        self._progress_message.setProgress(0)
        self._progress_message.show()
Exemple #56
0
    def run(self):
        super().run()

        if not self._result:
            self._add_to_recent_files = False  # Failed to read any models due to error.
            self._result = []

        # Scale down to maximum bounds size if that is available
        if hasattr(self._application.getController().getScene(),
                   "_maximum_bounds"):
            for node in self._result:
                max_bounds = self._application.getController().getScene(
                )._maximum_bounds
                node._resetAABB()
                build_bounds = node.getBoundingBox()

                if build_bounds is None or max_bounds is None:
                    continue

                if self._application.getInstance().getPreferences().getValue(
                        "mesh/scale_to_fit"
                ) == True or self._application.getInstance().getPreferences(
                ).getValue("mesh/scale_tiny_meshes") == True:
                    scale_factor_width = max_bounds.width / build_bounds.width
                    scale_factor_height = max_bounds.height / build_bounds.height
                    scale_factor_depth = max_bounds.depth / build_bounds.depth
                    scale_factor = min(scale_factor_width, scale_factor_depth,
                                       scale_factor_height)
                    if self._application.getInstance().getPreferences(
                    ).getValue("mesh/scale_to_fit") == True and (
                            scale_factor_width < 1 or scale_factor_height < 1
                            or scale_factor_depth < 1
                    ):  # Use scale factor to scale large object down
                        # Ignore scaling on models which are less than 1.25 times bigger than the build volume
                        ignore_factor = 1.25
                        if 1 / scale_factor < ignore_factor:
                            Logger.log(
                                "i",
                                "Ignoring auto-scaling, because %.3d < %.3d" %
                                (1 / scale_factor, ignore_factor))
                            scale_factor = 1
                        pass
                    elif self._application.getInstance().getPreferences(
                    ).getValue("mesh/scale_tiny_meshes") == True and (
                            scale_factor_width > 100
                            and scale_factor_height > 100
                            and scale_factor_depth > 100):
                        # Round scale factor to lower factor of 10 to scale tiny object up (eg convert m to mm units)
                        try:
                            scale_factor = math.pow(
                                10,
                                math.floor(
                                    math.log(scale_factor) / math.log(10)))
                        except:
                            # In certain cases the scale_factor can be inf which can make this fail. Just use 1 instead.
                            scale_factor = 1
                    else:
                        scale_factor = 1

                    if scale_factor != 1:
                        scale_vector = Vector(scale_factor, scale_factor,
                                              scale_factor)
                        display_scale_factor = scale_factor * 100

                        scale_message = Message(i18n_catalog.i18nc(
                            "@info:status",
                            "Auto scaled model to {0}% of original size",
                            ("%i" % display_scale_factor)),
                                                title=i18n_catalog.i18nc(
                                                    "@info:title",
                                                    "Scaling Object"))

                        try:
                            node.scale(scale_vector)
                            scale_message.show()
                        except Exception:
                            Logger.logException(
                                "e",
                                "While auto-scaling an exception has been raised"
                            )
Exemple #57
0
class SliceInfo(Extension):
    def __init__(self):
        super().__init__()
        Application.getInstance().getOutputDeviceManager(
        ).writeStarted.connect(self._onWriteStarted)
        Preferences.getInstance().addPreference("info/send_slice_info", True)
        Preferences.getInstance().addPreference("info/asked_send_slice_info",
                                                False)

        if not Preferences.getInstance().getValue(
                "info/asked_send_slice_info"):
            self.send_slice_info_message = Message(catalog.i18nc(
                "@info",
                "Cura automatically sends slice info. You can disable this in preferences"
            ),
                                                   lifetime=0,
                                                   dismissable=False)
            self.send_slice_info_message.addAction(
                "Dismiss", catalog.i18nc("@action:button", "Dismiss"), None,
                "")
            self.send_slice_info_message.actionTriggered.connect(
                self.messageActionTriggered)
            self.send_slice_info_message.show()

    def messageActionTriggered(self, message_id, action_id):
        self.send_slice_info_message.hide()
        Preferences.getInstance().setValue("info/asked_send_slice_info", True)

    def _onWriteStarted(self, output_device):
        if not Preferences.getInstance().getValue("info/send_slice_info"):
            return  # Do nothing, user does not want to send data
        settings = Application.getInstance().getMachineManager(
        ).getActiveProfile()

        # Load all machine definitions and put them in machine_settings dict
        #setting_file_name = Application.getInstance().getActiveMachineInstance().getMachineSettings()._json_file
        machine_settings = {}
        #with open(setting_file_name, "rt", -1, "utf-8") as f:
        #    data = json.load(f, object_pairs_hook = collections.OrderedDict)
        #machine_settings[os.path.basename(setting_file_name)] = copy.deepcopy(data)
        active_machine_definition = Application.getInstance(
        ).getMachineManager().getActiveMachineInstance().getMachineDefinition(
        )
        data = active_machine_definition._json_data
        # Loop through inherited json files
        setting_file_name = active_machine_definition._path
        while True:
            if "inherits" in data:
                inherited_setting_file_name = os.path.dirname(
                    setting_file_name) + "/" + data["inherits"]
                with open(inherited_setting_file_name, "rt", -1, "utf-8") as f:
                    data = json.load(f,
                                     object_pairs_hook=collections.OrderedDict)
                machine_settings[os.path.basename(
                    inherited_setting_file_name)] = copy.deepcopy(data)
            else:
                break

        profile_values = settings.getChangedSettings()

        # Get total material used (in mm^3)
        print_information = Application.getInstance().getPrintInformation()
        material_radius = 0.5 * settings.getSettingValue("material_diameter")
        material_used = math.pi * material_radius * material_radius * print_information.materialAmount  #Volume of material used

        # Get model information (bounding boxes, hashes and transformation matrix)
        models_info = []
        for node in DepthFirstIterator(Application.getInstance().getController(
        ).getScene().getRoot()):
            if type(node) is SceneNode and node.getMeshData(
            ) and node.getMeshData().getVertices() is not None:
                if not getattr(node, "_outside_buildarea", False):
                    model_info = {}
                    model_info["hash"] = node.getMeshData().getHash()
                    model_info["bounding_box"] = {}
                    model_info["bounding_box"]["minimum"] = {}
                    model_info["bounding_box"]["minimum"][
                        "x"] = node.getBoundingBox().minimum.x
                    model_info["bounding_box"]["minimum"][
                        "y"] = node.getBoundingBox().minimum.y
                    model_info["bounding_box"]["minimum"][
                        "z"] = node.getBoundingBox().minimum.z

                    model_info["bounding_box"]["maximum"] = {}
                    model_info["bounding_box"]["maximum"][
                        "x"] = node.getBoundingBox().maximum.x
                    model_info["bounding_box"]["maximum"][
                        "y"] = node.getBoundingBox().maximum.y
                    model_info["bounding_box"]["maximum"][
                        "z"] = node.getBoundingBox().maximum.z
                    model_info["transformation"] = str(
                        node.getWorldTransformation().getData())

                    models_info.append(model_info)

        # Bundle the collected data
        submitted_data = {
            "processor": platform.processor(),
            "machine": platform.machine(),
            "platform": platform.platform(),
            "machine_settings": json.dumps(machine_settings),
            "version": Application.getInstance().getVersion(),
            "modelhash": "None",
            "printtime": str(print_information.currentPrintTime),
            "filament": material_used,
            "language": Preferences.getInstance().getValue("general/language"),
            "materials_profiles ": {}
        }

        # Convert data to bytes
        submitted_data = urllib.parse.urlencode(submitted_data)
        binary_data = submitted_data.encode("utf-8")

        # Submit data
        try:
            f = urllib.request.urlopen(
                "https://stats.youmagine.com/curastats/slice",
                data=binary_data,
                timeout=1)
        except Exception as e:
            print("Exception occured", e)

        f.close()
Exemple #58
0
class MKSOutputDevicePlugin(QObject, OutputDevicePlugin):
    def __init__(self):
        super().__init__()
        self._zero_conf = None
        self._browser = None
        self._printers = {}
        self._discovered_devices = {}

        self._error_message = None

        self._old_printers = []

        self.addPrinterSignal.connect(self.addPrinter)
        self.removePrinterSignal.connect(self.removePrinter)

        self._preferences = Application.getInstance().getPreferences()
        self._preferences.addPreference("mkswifi/manual_instances", "")
        self._preferences.addPreference("local_file/last_used_type", "")
        self._preferences.addPreference("local_file/dialog_save_path", "")
        self._manual_instances = self._preferences.getValue(
            "mkswifi/manual_instances").split(",")
        Application.getInstance().globalContainerStackChanged.connect(
            self.reCheckConnections)

        self._service_changed_request_queue = Queue()
        self._service_changed_request_event = Event()
        self._service_changed_request_thread = Thread(
            target=self._handleOnServiceChangedRequests, daemon=True)
        self._service_changed_request_thread.start()

        self._changestage = False

    addPrinterSignal = Signal()
    removePrinterSignal = Signal()
    printerListChanged = Signal()

    def start(self):

        self.startDiscovery()

    def startDiscovery(self):
        self.stop()
        self.getOutputDeviceManager().addOutputDevice(
            SaveOutputDevice.SaveOutputDevice())
        if self._browser:
            self._browser.cancel()
            self._browser = None
            self._old_printers = [
                printer_name for printer_name in self._printers
            ]
            self._printers = {}
            self.printerListChanged.emit()
        self._zero_conf = Zeroconf()
        self._browser = ServiceBrowser(self._zero_conf, u'_mks._tcp.local.',
                                       [self._appendServiceChangedRequest])
        for address in self._manual_instances:
            if address:
                self.addManualPrinter(address)

    def addManualPrinter(self, address):
        if address not in self._manual_instances:
            self._manual_instances.append(address)
            self._preferences.setValue("mkswifi/manual_instances",
                                       ",".join(self._manual_instances))

        instance_name = "manual:%s" % address
        properties = {
            b"name": address.encode("utf-8"),
            b"address": address.encode("utf-8"),
            b"manual": b"true",
            b"incomplete": b"false"
        }

        if instance_name not in self._printers:
            # Add a preliminary printer instance
            self.addPrinter(instance_name, address, properties)

        # self.checkManualPrinter(address)
        # self.checkClusterPrinter(address)

    def removeManualPrinter(self, key, address=None):
        if key in self._printers:
            if not address:
                address = self._printers[key].ipAddress
            self.removePrinter(key)

        if address in self._manual_instances:
            self._manual_instances.remove(address)
            self._preferences.setValue("mkswifi/manual_instances",
                                       ",".join(self._manual_instances))

    def stop(self):
        # self.getOutputDeviceManager().removeOutputDevice("save_with_screenshot")
        if self._zero_conf is not None:
            Logger.log("d", "zeroconf close...")
            self._zero_conf.close()

    def getPrinters(self):
        return self._printers

    def disConnections(self, key):
        Logger.log("d", "disConnections change %s" % key)
        # for keys in self._printers:
        #     if self._printers[key].isConnected():
        #         Logger.log("d", "Closing connection [%s]..." % key)
        if key in self._printers:
            self._printers[key].disconnect()
            # self._printers[key].connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
            self.getOutputDeviceManager().removeOutputDevice(key)
        preferences = Application.getInstance().getPreferences()
        preferences.addPreference("mkswifi/stopupdate", "True")

    def reCheckConnections(self):
        active_machine = Application.getInstance().getGlobalContainerStack()
        Logger.log(
            "d", "GlobalContainerStack change %s" %
            active_machine.getMetaDataEntry("mks_network_key"))
        if not active_machine:
            return

        for key in self._printers:
            if key == active_machine.getMetaDataEntry("mks_network_key"):
                if not self._printers[key].isConnected():
                    Logger.log("d", "Connecting [%s]..." % key)
                    self._printers[key].connect()
                    self._printers[key].connectionStateChanged.connect(
                        self._onPrinterConnectionStateChanged)
            else:
                if self._printers[key].isConnected():
                    Logger.log("d", "Closing connection [%s]..." % key)
                    self._printers[key].disconnect()
                    self._printers[key].connectionStateChanged.disconnect(
                        self._onPrinterConnectionStateChanged)

    def addPrinter(self, name, address, properties):
        printer = MKSOutputDevice.MKSOutputDevice(name, address, properties)
        # self._api_prefix = "/"
        # printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix)
        self._printers[printer.getKey()] = printer
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack and printer.getKey(
        ) == global_container_stack.getMetaDataEntry("mks_network_key"):
            if printer.getKey(
            ) not in self._old_printers:  # Was the printer already connected, but a re-scan forced?
                Logger.log("d",
                           "addPrinter, connecting [%s]..." % printer.getKey())
                self._printers[printer.getKey()].connect()
                printer.connectionStateChanged.connect(
                    self._onPrinterConnectionStateChanged)
        self.printerListChanged.emit()

    def removePrinter(self, name):
        printer = self._printers.pop(name, None)
        if printer:
            if printer.isConnected():
                printer.disconnect()
                printer.connectionStateChanged.disconnect(
                    self._onPrinterConnectionStateChanged)
                Logger.log("d", "removePrinter, disconnecting [%s]..." % name)
        self.printerListChanged.emit()

    def printertrytoconnect(self):
        Logger.log("d", "mks printertrytoconnect")
        self._changestage = True

    def _onPrinterConnectionStateChanged(self, key):
        if key not in self._printers:
            return
        # Logger.log("d", "mks add output device %s" % self._printers[key].isConnected())
        if self._printers[key].isConnected():
            # Logger.log("d", "mks add output device--------ok--------- %s" % self._printers[key].isConnected())
            if self._error_message:
                self._error_message.hide()
            name = "Printer connect success"
            if CuraApplication.getInstance().getPreferences().getValue(
                    "general/language") == "zh_CN":
                name = "打印机连接成功"
            else:
                name = "Printer connect success"
            self._error_message = Message(name)
            self._error_message.show()
            self.getOutputDeviceManager().addOutputDevice(self._printers[key])
            # preferences = Application.getInstance().getPreferences()
            # if preferences.getValue("mkswifi/changestage"):
            #     preferences.addPreference("mkswifi/changestage", "False")
            #     CuraApplication.getInstance().getController().setActiveStage("MonitorStage")
        else:
            # self.getOutputDeviceManager().removeOutputDevice(key)
            global_container_stack = CuraApplication.getInstance(
            ).getGlobalContainerStack()
            if global_container_stack:
                meta_data = global_container_stack.getMetaData()
                if "mks_network_key" in meta_data:
                    localkey = global_container_stack.getMetaDataEntry(
                        "mks_network_key")
                    # global_container_stack.setMetaDataEntry("mks_network_key", key)
                    # global_container_stack.removeMetaDataEntry(
                    # "network_authentication_id")
                    # global_container_stack.removeMetaDataEntry(
                    # "network_authentication_key")
                    # Logger.log("d", "mks localkey--------ok--------- %s" % localkey)
                    # Logger.log("d", "mks key--------ok--------- %s" % key)
                    if localkey != key and key in self._printers:
                        # self.getOutputDeviceManager().connect()
                        self.getOutputDeviceManager().removeOutputDevice(key)
        # else:
        #     if self._error_message:
        #         self._error_message.hide()
        #     self._error_message = Message(i18n_catalog.i18nc("@info:status", "Printer connect failed"))
        #     self._error_message.show()
        # else:
        #     Logger.log("d", "mks add output device--------ok--------- %s" % self._printers[key].isConnected())
        #     self._printers[key].disconnect()
        # self._printers[key].connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)

    def _onServiceChanged(self, zeroconf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            Logger.log("d", "Bonjour service added: %s" % name)

            # First try getting info from zeroconf cache
            info = ServiceInfo(service_type, name, properties={})
            for record in zeroconf.cache.entries_with_name(name.lower()):
                info.update_record(zeroconf, time.time(), record)

            for record in zeroconf.cache.entries_with_name(info.server):
                info.update_record(zeroconf, time.time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                Logger.log("d", "Trying to get address of %s", name)
                info = zeroconf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type", None)
                if type_of_device:
                    if type_of_device == b"printer":
                        address = '.'.join(map(lambda n: str(n), info.address))
                        if address in self._excluded_addresses:
                            Logger.log(
                                "d",
                                "The IP address %s of the printer \'%s\' is not correct. Trying to reconnect.",
                                address, name)
                            return False  # When getting the localhost IP, then try to reconnect
                        self.addPrinterSignal.emit(str(name), address,
                                                   info.properties)
                    else:
                        Logger.log(
                            "w",
                            "The type of the found device is '%s', not 'printer'! Ignoring.."
                            % type_of_device)
            else:
                Logger.log("w", "Could not get information about %s" % name)
                return False

        elif state_change == ServiceStateChange.Removed:
            Logger.log("d", "Bonjour service removed: %s" % name)
            self.removePrinterSignal.emit(str(name))

        return True

    def _appendServiceChangedRequest(self, zeroconf, service_type, name,
                                     state_change):
        # append the request and set the event so the event handling thread can pick it up
        item = (zeroconf, service_type, name, state_change)
        self._service_changed_request_queue.put(item)
        self._service_changed_request_event.set()

    def _handleOnServiceChangedRequests(self):
        while True:
            # wait for the event to be set
            self._service_changed_request_event.wait(timeout=5.0)
            # stop if the application is shutting down
            if Application.getInstance().isShuttingDown():
                return

            self._service_changed_request_event.clear()

            # handle all pending requests
            reschedule_requests = [
            ]  # a list of requests that have failed so later they will get re-scheduled
            while not self._service_changed_request_queue.empty():
                request = self._service_changed_request_queue.get()
                zeroconf, service_type, name, state_change = request
                try:
                    result = self._onServiceChanged(zeroconf, service_type,
                                                    name, state_change)
                    if not result:
                        reschedule_requests.append(request)
                except Exception:
                    Logger.logException(
                        "e",
                        "Failed to get service info for [%s] [%s], the request will be rescheduled",
                        service_type, name)
                    reschedule_requests.append(request)

            # re-schedule the failed requests if any
            if reschedule_requests:
                for request in reschedule_requests:
                    self._service_changed_request_queue.put(request)

    @pyqtSlot()
    def openControlPanel(self):
        Logger.log("d", "Opening print jobs web UI...")
        selected_device = self.getOutputDeviceManager().getActiveDevice()
        self._monitor_view_qml_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)), "MonitorItem4x.qml")
        self.__additional_components_view = Application.getInstance(
        ).createQmlComponent(self._monitor_view_qml_path, {"manager": self})
    def requestWrite(self, nodes, file_name = None, limit_mimetypes = None, file_handler = None, **kwargs):
        """Request the specified nodes to be written to a file.
        
        :param nodes: A collection of scene nodes that should be written to the
        file.
        :param file_name: A suggestion for the file name to write
        to. Can be freely ignored if providing a file name makes no sense.
        :param limit_mimetypes: Should we limit the available MIME types to the
        MIME types available to the currently active machine?
        :param kwargs: Keyword arguments.
        """

        if self._writing:
            raise OutputDeviceError.DeviceBusyError()

        # Set up and display file dialog
        dialog = QFileDialog()

        dialog.setWindowTitle(catalog.i18nc("@title:window", "Save to File"))
        dialog.setFileMode(QFileDialog.AnyFile)
        dialog.setAcceptMode(QFileDialog.AcceptSave)

        # Ensure platform never ask for overwrite confirmation since we do this ourselves
        dialog.setOption(QFileDialog.DontConfirmOverwrite)

        if sys.platform == "linux" and "KDE_FULL_SESSION" in os.environ:
            dialog.setOption(QFileDialog.DontUseNativeDialog)

        filters = []
        mime_types = []
        selected_filter = None

        if "preferred_mimetypes" in kwargs and kwargs["preferred_mimetypes"] is not None:
            preferred_mimetypes = kwargs["preferred_mimetypes"]
        else:
            preferred_mimetypes = Application.getInstance().getPreferences().getValue("local_file/last_used_type")
        preferred_mimetype_list = preferred_mimetypes.split(";")

        if not file_handler:
            file_handler = Application.getInstance().getMeshFileHandler()

        file_types = file_handler.getSupportedFileTypesWrite()

        file_types.sort(key = lambda k: k["description"])
        if limit_mimetypes:
            file_types = list(filter(lambda i: i["mime_type"] in limit_mimetypes, file_types))

        file_types = [ft for ft in file_types if not ft["hide_in_file_dialog"]]

        if len(file_types) == 0:
            Logger.log("e", "There are no file types available to write with!")
            raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:warning", "There are no file types available to write with!"))

        # Find the first available preferred mime type
        preferred_mimetype = None
        for mime_type in preferred_mimetype_list:
            if any(ft["mime_type"] == mime_type for ft in file_types):
                preferred_mimetype = mime_type
                break

        extension_added = False
        for item in file_types:
            type_filter = "{0} (*.{1})".format(item["description"], item["extension"])
            filters.append(type_filter)
            mime_types.append(item["mime_type"])
            if preferred_mimetype == item["mime_type"]:
                selected_filter = type_filter
                if file_name and not extension_added:
                    extension_added = True
                    file_name += "." + item["extension"]

        # CURA-6411: This code needs to be before dialog.selectFile and the filters, because otherwise in macOS (for some reason) the setDirectory call doesn't work.
        stored_directory = Application.getInstance().getPreferences().getValue("local_file/dialog_save_path")
        dialog.setDirectory(stored_directory)

        # Add the file name before adding the extension to the dialog
        if file_name is not None:
            dialog.selectFile(file_name)

        dialog.setNameFilters(filters)
        if selected_filter is not None:
            dialog.selectNameFilter(selected_filter)

        if not dialog.exec_():
            raise OutputDeviceError.UserCanceledError()

        save_path = dialog.directory().absolutePath()
        Application.getInstance().getPreferences().setValue("local_file/dialog_save_path", save_path)

        selected_type = file_types[filters.index(dialog.selectedNameFilter())]
        Application.getInstance().getPreferences().setValue("local_file/last_used_type", selected_type["mime_type"])

        # Get file name from file dialog
        file_name = dialog.selectedFiles()[0]
        Logger.log("d", "Writing to [%s]..." % file_name)
        
        if os.path.exists(file_name):
            result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"), catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
            if result == QMessageBox.No:
                raise OutputDeviceError.UserCanceledError()

        self.writeStarted.emit(self)

        # Actually writing file
        if file_handler:
            file_writer = file_handler.getWriter(selected_type["id"])
        else:
            file_writer = Application.getInstance().getMeshFileHandler().getWriter(selected_type["id"])

        try:
            mode = selected_type["mode"]
            if mode == MeshWriter.OutputMode.TextMode:
                Logger.log("d", "Writing to Local File %s in text mode", file_name)
                stream = open(file_name, "wt", encoding = "utf-8")
            elif mode == MeshWriter.OutputMode.BinaryMode:
                Logger.log("d", "Writing to Local File %s in binary mode", file_name)
                stream = open(file_name, "wb")
            else:
                Logger.log("e", "Unrecognised OutputMode.")
                return None

            job = WriteFileJob(file_writer, stream, nodes, mode)
            job.setFileName(file_name)
            job.setAddToRecentFiles(True)  # The file will be added into the "recent files" list upon success
            job.progress.connect(self._onJobProgress)
            job.finished.connect(self._onWriteJobFinished)

            message = Message(catalog.i18nc("@info:progress Don't translate the XML tags <filename>!", "Saving to <filename>{0}</filename>").format(file_name),
                              0, False, -1 , catalog.i18nc("@info:title", "Saving"))
            message.show()

            job.setMessage(message)
            self._writing = True
            job.start()
        except PermissionError as e:
            Logger.log("e", "Permission denied when trying to write to %s: %s", file_name, str(e))
            raise OutputDeviceError.PermissionDeniedError(catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Permission denied when trying to save <filename>{0}</filename>").format(file_name)) from e
        except OSError as e:
            Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e))
            raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format()) from e
Exemple #60
0
    def processGCodeStream(self, stream: str) -> Optional[CuraSceneNode]:
        Logger.log("d", "Preparing to load GCode")
        self._cancelled = False
        # We obtain the filament diameter from the selected extruder to calculate line widths
        global_stack = CuraApplication.getInstance().getGlobalContainerStack()

        if not global_stack:
            return None

        self._filament_diameter = global_stack.extruders[str(
            self._extruder_number)].getProperty("material_diameter", "value")

        scene_node = CuraSceneNode()

        gcode_list = []
        self._is_layers_in_file = False

        self._extruder_offsets = self._extruderOffsets(
        )  # dict with index the extruder number. can be empty

        ##############################################################################################
        ##  This part is where the action starts
        ##############################################################################################
        file_lines = 0
        current_line = 0
        for line in stream.split("\n"):
            file_lines += 1
            gcode_list.append(line + "\n")
            if not self._is_layers_in_file and line[:len(
                    self._layer_keyword)] == self._layer_keyword:
                self._is_layers_in_file = True

        file_step = max(math.floor(file_lines / 100), 1)

        self._clearValues()

        self._message = Message(catalog.i18nc("@info:status",
                                              "Parsing G-code"),
                                lifetime=0,
                                title=catalog.i18nc("@info:title",
                                                    "G-code Details"))

        assert (self._message is not None)  # use for typing purposes
        self._message.setProgress(0)
        self._message.show()

        Logger.log("d", "Parsing Gcode...")

        current_position = Position(0, 0, 0, 0, [0])
        current_path = []  #type: List[List[float]]
        min_layer_number = 0
        negative_layers = 0
        previous_layer = 0

        for line in stream.split("\n"):
            if self._cancelled:
                Logger.log("d", "Parsing Gcode file cancelled")
                return None
            current_line += 1

            if current_line % file_step == 0:
                self._message.setProgress(
                    math.floor(current_line / file_lines * 100))
                Job.yieldThread()
            if len(line) == 0:
                continue

            if line.find(self._type_keyword) == 0:
                type = line[len(self._type_keyword):].strip()
                if type == "WALL-INNER":
                    self._layer_type = LayerPolygon.InsetXType
                elif type == "WALL-OUTER":
                    self._layer_type = LayerPolygon.Inset0Type
                elif type == "SKIN":
                    self._layer_type = LayerPolygon.SkinType
                elif type == "SKIRT":
                    self._layer_type = LayerPolygon.SkirtType
                elif type == "SUPPORT":
                    self._layer_type = LayerPolygon.SupportType
                elif type == "FILL":
                    self._layer_type = LayerPolygon.InfillType
                else:
                    Logger.log(
                        "w",
                        "Encountered a unknown type (%s) while parsing g-code.",
                        type)

            # When the layer change is reached, the polygon is computed so we have just one layer per extruder
            if self._is_layers_in_file and line[:len(self._layer_keyword
                                                     )] == self._layer_keyword:
                try:
                    layer_number = int(line[len(self._layer_keyword):])
                    self._createPolygon(
                        self._current_layer_thickness, current_path,
                        self._extruder_offsets.get(self._extruder_number,
                                                   [0, 0]))
                    current_path.clear()
                    # Start the new layer at the end position of the last layer
                    current_path.append([
                        current_position.x, current_position.y,
                        current_position.z, current_position.f,
                        current_position.e[self._extruder_number],
                        LayerPolygon.MoveCombingType
                    ])

                    # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
                    # as in ProcessSlicedLayersJob
                    if layer_number < min_layer_number:
                        min_layer_number = layer_number
                    if layer_number < 0:
                        layer_number += abs(min_layer_number)
                        negative_layers += 1
                    else:
                        layer_number += negative_layers

                    # In case there is a gap in the layer count, empty layers are created
                    for empty_layer in range(previous_layer + 1, layer_number):
                        self._createEmptyLayer(empty_layer)

                    self._layer_number = layer_number
                    previous_layer = layer_number
                except:
                    pass

            # This line is a comment. Ignore it (except for the layer_keyword)
            if line.startswith(";"):
                continue

            G = self._getInt(line, "G")
            if G is not None:
                # When find a movement, the new posistion is calculated and added to the current_path, but
                # don't need to create a polygon until the end of the layer
                current_position = self.processGCode(G, line, current_position,
                                                     current_path)
                continue

            # When changing the extruder, the polygon with the stored paths is computed
            if line.startswith("T"):
                T = self._getInt(line, "T")
                if T is not None:
                    self._createPolygon(
                        self._current_layer_thickness, current_path,
                        self._extruder_offsets.get(self._extruder_number,
                                                   [0, 0]))
                    current_path.clear()

                    # When changing tool, store the end point of the previous path, then process the code and finally
                    # add another point with the new position of the head.
                    current_path.append([
                        current_position.x, current_position.y,
                        current_position.z, current_position.f,
                        current_position.e[self._extruder_number],
                        LayerPolygon.MoveCombingType
                    ])
                    current_position = self.processTCode(
                        T, line, current_position, current_path)
                    current_path.append([
                        current_position.x, current_position.y,
                        current_position.z, current_position.f,
                        current_position.e[self._extruder_number],
                        LayerPolygon.MoveCombingType
                    ])

            if line.startswith("M"):
                M = self._getInt(line, "M")
                self.processMCode(M, line, current_position, current_path)

        # "Flush" leftovers. Last layer paths are still stored
        if len(current_path) > 1:
            if self._createPolygon(
                    self._current_layer_thickness, current_path,
                    self._extruder_offsets.get(self._extruder_number, [0, 0])):
                self._layer_number += 1
                current_path.clear()

        material_color_map = numpy.zeros((8, 4), dtype=numpy.float32)
        material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
        material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
        material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
        material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
        material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
        material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
        material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
        material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
        layer_mesh = self._layer_data_builder.build(material_color_map)
        decorator = LayerDataDecorator()
        decorator.setLayerData(layer_mesh)
        scene_node.addDecorator(decorator)

        gcode_list_decorator = GCodeListDecorator()
        gcode_list_decorator.setGCodeList(gcode_list)
        scene_node.addDecorator(gcode_list_decorator)

        # gcode_dict stores gcode_lists for a number of build plates.
        active_build_plate_id = CuraApplication.getInstance(
        ).getMultiBuildPlateModel().activeBuildPlate
        gcode_dict = {active_build_plate_id: gcode_list}
        CuraApplication.getInstance().getController().getScene(
        ).gcode_dict = gcode_dict  #type: ignore #Because gcode_dict is generated dynamically.

        Logger.log("d", "Finished parsing Gcode")
        self._message.hide()

        if self._layer_number == 0:
            Logger.log("w", "File doesn't contain any valid layers")

        settings = CuraApplication.getInstance().getGlobalContainerStack()
        if not settings.getProperty("machine_center_is_zero", "value"):
            machine_width = settings.getProperty("machine_width", "value")
            machine_depth = settings.getProperty("machine_depth", "value")
            scene_node.setPosition(
                Vector(-machine_width / 2, 0, machine_depth / 2))

        Logger.log("d", "GCode loading finished")

        if CuraApplication.getInstance().getPreferences().getValue(
                "gcodereader/show_caution"):
            caution_message = Message(catalog.i18nc(
                "@info:generic",
                "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."
            ),
                                      lifetime=0,
                                      title=catalog.i18nc(
                                          "@info:title", "G-code Details"))
            caution_message.show()

        # The "save/print" button's state is bound to the backend state.
        backend = CuraApplication.getInstance().getBackend()
        backend.backendStateChange.emit(Backend.BackendState.Disabled)

        return scene_node