예제 #1
0
    def _onFinished(self, job):
        if self._stream:
            # Explicitly closing the stream flushes the write-buffer
            try:
                self._stream.close()
                self._stream = None
            except:
                Logger.logException("w", "An execption occured while trying to write to removable drive.")
                message = Message(catalog.i18nc("@info:status", "Could not save to removable drive {0}: {1}").format(self.getName(),str(job.getError())),
                                  title = catalog.i18nc("@info:title", "Error"))
                message.show()
                self.writeError.emit(self)
                return

        self._writing = False
        self.writeFinished.emit(self)
        if job.getResult():
            message = Message(catalog.i18nc("@info:status", "Saved to Removable Drive {0} as {1}").format(self.getName(), os.path.basename(job.getFileName())), title = catalog.i18nc("@info:title", "File Saved"))
            message.addAction("eject", catalog.i18nc("@action:button", "Eject"), "eject", catalog.i18nc("@action", "Eject removable device {0}").format(self.getName()))
            message.actionTriggered.connect(self._onActionTriggered)
            message.show()
            self.writeSuccess.emit(self)
        else:
            message = Message(catalog.i18nc("@info:status", "Could not save to removable drive {0}: {1}").format(self.getName(), str(job.getError())), title = catalog.i18nc("@info:title", "Warning"))
            message.show()
            self.writeError.emit(self)
        job.getStream().close()
예제 #2
0
    def _showSyncNewMaterialsMessage(self) -> None:
        sync_materials_message = Message(
                text = catalog.i18nc("@action:button",
                                     "Please sync the material profiles with your printers before starting to print."),
                title = catalog.i18nc("@action:button", "New materials installed"),
                message_type = Message.MessageType.WARNING,
                lifetime = 0
        )

        sync_materials_message.addAction(
                "sync",
                name = catalog.i18nc("@action:button", "Sync materials"),
                icon = "",
                description = "Sync your newly installed materials with your printers.",
                button_align = Message.ActionButtonAlignment.ALIGN_RIGHT
        )

        sync_materials_message.addAction(
                "learn_more",
                name = catalog.i18nc("@action:button", "Learn more"),
                icon = "",
                description = "Learn more about syncing your newly installed materials with your printers.",
                button_align = Message.ActionButtonAlignment.ALIGN_LEFT,
                button_style = Message.ActionButtonStyle.LINK
        )
        sync_materials_message.actionTriggered.connect(self._onSyncMaterialsMessageActionTriggered)

        # Show the message only if there are printers that support material export
        container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
        global_stacks = container_registry.findContainerStacks(type = "machine")
        if any([stack.supportsMaterialExport for stack in global_stacks]):
            sync_materials_message.show()
예제 #3
0
    def _onWriteJobFinished(self, job):
        if hasattr(job, "_message"):
            job._message.hide()
            job._message = None

        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()
예제 #4
0
    def run(self):
        if not self.url:
            Logger.log("e", "Can not check for a new release. URL not set!")
        no_new_version = True

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

        try:
            req = urllib.request.Request(self.url, headers={'User-Agent': 'Mozilla/5.0'})
            latest_version_file = urllib.request.urlopen(req)
        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

        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 self.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 self.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("w", "Could not find version information or download url for update.")
            else:
                Logger.log("w", "Did not find any version information for %s." % application_name)
        except Exception:
            Logger.logException("e", "Exception in update checker while parsing the JSON file.")
            Message(i18n_catalog.i18nc("@info", "An exception occured while parsing the JSON file.")).show()
            no_new_version = False # Just to suppress the message below.

        if no_new_version and not self.silent:
            Message(i18n_catalog.i18nc("@info", "No new version was found.")).show()
예제 #5
0
def test_addAction():
    message = Message()
    message.addAction(action_id="blarg",
                      name="zomg",
                      icon="NO ICON",
                      description="SuperAwesomeMessage")

    assert len(message.getActions()) == 1
예제 #6
0
    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()
예제 #7
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 _onNetworkFinished(self, reply):
        self.setConnectionState(ConnectionState.connected)
        if reply.operation() == self._qnam.PostOperation:
            Logger.log("i", "_onNetworkFinished reply: %s",
                       repr(reply.readAll()))
            Logger.log("i", "_onNetworkFinished reply.error(): %s",
                       repr(reply.error()))

            self._stage = OutputStage.ready
            if self._message:
                self._message.hide()
            self._message = None
            self._update_timer.start()

            self.writeFinished.emit(self)
            if reply.error():
                message = Message(
                    catalog.i18nc("@info:status",
                                  "Could not save to {0}: {1}").format(
                                      self.getName(),
                                      str(reply.errorString())))
                message.show()
                self.writeError.emit(self)
            else:
                message = Message(
                    catalog.i18nc("@info:status",
                                  "Saved to {0} as {1}").format(
                                      self.getName(),
                                      os.path.basename(self._fileName)))
                message.addAction(
                    "open_browser",
                    catalog.i18nc("@action:button", "Open Browser"), "globe",
                    catalog.i18nc("@info:tooltip", "Open browser to printer."))
                message.actionTriggered.connect(self._onMessageActionTriggered)
                message.show()
                self.writeSuccess.emit(self)
            self._cleanupRequest()
        elif "status" in reply.url().toString():
            config = ConfigParser(allow_no_value=True)
            ini_string = str(reply.readAll(),
                             'utf8')  # convert qbytearray to str
            config.readfp(io.StringIO(ini_string))
            lineLastReceived = config.get("Status", "lineLastReceived")
            Logger.log("d", lineLastReceived)
            try:
                res = re.findall('T0:([0-9]*?\.[0-9])', lineLastReceived, re.S)
                hotend_temperature = float(res[0])
                # Logger.log("d", hotend_temperature)
                self._setHotendTemperature(index=0,
                                           temperature=hotend_temperature
                                           )  # TODO: Handle multiple extruders
            except:
                pass
            Logger.log("d", config.get("Status", "jobName"))
    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 save_gcode(self, file_name, _gcode):
     global_container_stack = Application.getInstance(
     ).getGlobalContainerStack()
     if not global_container_stack:
         return
     job_name = Application.getInstance().getPrintInformation(
     ).jobName.strip()
     if job_name is "":
         job_name = "untitled_print"
     job_name = "%s.gcode" % job_name
     image = utils.take_screenshot()
     # Logger.log("d", os.path.abspath("")+"\\test.png")
     message = Message(
         catalog.i18nc(
             "@info:status",
             "Saving to <filename>{0}</filename>").format(file_name), 0,
         False, -1)
     try:
         message.show()
         save_file = open(file_name, "w")
         if image:
             save_file.write(
                 utils.add_screenshot(image, 100, 100, ";simage:"))
             save_file.write(
                 utils.add_screenshot(image, 200, 200, ";;gimage:"))
             save_file.write("\r")
         for line in _gcode:
             save_file.write(line)
         save_file.close()
         message.hide()
         self.writeFinished.emit(self)
         self.writeSuccess.emit(self)
         message = Message(
             catalog.i18nc(
                 "@info:status",
                 "Saved to <filename>{0}</filename>").format(job_name))
         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(file_name)
         message.actionTriggered.connect(self._onMessageActionTriggered)
         message.show()
     except Exception as e:
         message.hide()
         message = Message(catalog.i18nc(
             "@info:status",
             "Could not save to <filename>{0}</filename>: <message>{1}</message>"
         ).format(file_name, str(e)),
                           lifetime=0)
         message.show()
         self.writeError.emit(self)
예제 #11
0
 def _handlePackageDiscrepancies(self) -> None:
     Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages")
     sync_message = Message(self._i18n_catalog.i18nc(
         "@info:generic",
         "\nDo you want to sync material and software packages with your account?"),
         title=self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
     sync_message.addAction("sync",
                            name=self._i18n_catalog.i18nc("@action:button", "Sync"),
                            icon="",
                            description="Sync your Cloud subscribed packages to your local environment.",
                            button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
     sync_message.actionTriggered.connect(self._onSyncButtonClicked)
     sync_message.show()
 def _showRequestSucceededMessage(self):
     confirmation_message_template = i18n_catalog.i18nc(
         "@info:status",
         "Sent {file_name} to group {cluster_name}."
     )
     file_name = os.path.basename(self._file_name).split(".")[0]
     message_text = confirmation_message_template.format(cluster_name = self.getName(), file_name = file_name)
     message = Message(text=message_text)
     button_text = i18n_catalog.i18nc("@action:button", "Show print jobs")
     button_tooltip = i18n_catalog.i18nc("@info:tooltip", "Opens the print jobs interface in your browser.")
     message.addAction("open_browser", button_text, "globe", button_tooltip)
     message.actionTriggered.connect(self._onMessageActionTriggered)
     message.show()
예제 #13
0
 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()
예제 #14
0
 def messageMaker(self):
     message = Message(
         catalog.i18nc(
             "@info:status",
             "New features are available for your Nautilus! It is recommended to update the firmware on your printer."
         ), 0)
     message.addAction(
         "download_config", catalog.i18nc("@action:button",
                                          "How to update"), "globe",
         catalog.i18nc("@info:tooltip",
                       "Open website to download new firmware"))
     message.actionTriggered.connect(self._onMessageActionTriggered)
     message.show()
예제 #15
0
class MaterialManager(QObject):
    ##  Creates the global values for the material manager to use.
    def __init__(self, parent=None):
        super().__init__(parent)

        #Material diameter changed warning message.
        self._material_diameter_warning_message = Message(
            catalog.i18nc(
                "@info:status Has a cancel button next to it.",
                "The selected material diameter causes the material to become incompatible with the current printer."
            ))
        self._material_diameter_warning_message.addAction(
            "Undo", catalog.i18nc("@action:button", "Undo"), None,
            catalog.i18nc("@action", "Undo changing the material diameter."))
        self._material_diameter_warning_message.actionTriggered.connect(
            self._materialWarningMessageAction)

    ##  Creates an instance of the MaterialManager.
    #
    #   This should only be called by PyQt to create the singleton instance of
    #   this class.
    @staticmethod
    def createMaterialManager(engine=None, script_engine=None):
        return MaterialManager()

    @pyqtSlot(str, str)
    def showMaterialWarningMessage(self, material_id, previous_diameter):
        self._material_diameter_warning_message.previous_diameter = previous_diameter  #Make sure that the undo button can properly undo the action.
        self._material_diameter_warning_message.material_id = material_id
        self._material_diameter_warning_message.show()

    ##  Called when clicking "undo" on the warning dialogue for disappeared
    #   materials.
    #
    #   This executes the undo action, restoring the material diameter.
    #
    #   \param button The identifier of the button that was pressed.
    def _materialWarningMessageAction(self, message, button):
        if button == "Undo":
            container_manager = ContainerManager.getInstance()
            container_manager.setContainerMetaDataEntry(
                self._material_diameter_warning_message.material_id,
                "properties/diameter",
                self._material_diameter_warning_message.previous_diameter)
            message.hide()
        else:
            Logger.log(
                "w",
                "Unknown button action for material diameter warning message: {action}"
                .format(action=button))
예제 #16
0
파일: Scene.py 프로젝트: jbkim/Uranium
    def _onFileChanged(self, file_path: str) -> None:
        if not os.path.isfile(self.file_path): #File doesn't exist any more.
            return

        #Multiple nodes may be loaded from the same file at different stages. Reload them all.
        from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator #To find which nodes to reload when files have changed.
        modified_nodes = (node for node in DepthFirstIterator(self.getRoot()) if node.getMeshData() and node.getMeshData().getFileName() == file_path)

        if modified_nodes:
            message = Message(i18n_catalog.i18nc("@info", "Would you like to reload {filename}?").format(filename = os.path.basename(self._file_name)),
                              title = i18n_catalog.i18nc("@info:title", "File has been modified"))
            message.addAction("reload", i18n_catalog.i18nc("@action:button", "Reload"), icon = None, description = i18n_catalog.i18nc("@action:description", "This will trigger the modified files to reload from disk."))
            message.actionTriggered.connect(functools.partialmethod(self._reloadNodes, modified_nodes))
            message.show()
예제 #17
0
    def run(self):
        self._download_url = None  # Reset download ur.
        if not self._url:
            Logger.log("e", "Can not check for a new release. URL not set!")
            return

        try:
            request = urllib.request.Request(self._url)
            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"]:
                # 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!", "To ensure that your {machine_name} is equipped with the latest features it is recommended to update the firmware regularly. This can be done on the {machine_name} (when connected to the network) or via USB.").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", "Download"), "[no_icon]", "[no_description]")

                    # If we do this in a cool way, the download url should be available in the JSON file
                    self._download_url = "https://ultimaker.com/en/resources/20500-upgrade-firmware"
                    message.actionTriggered.connect(self.actionTriggered)
                    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
예제 #18
0
 def _onUpdateRequired(self):
     #NautilusUpdate.NautilusUpdate().thingsChanged()
     message = Message(
         catalog.i18nc(
             "@info:status",
             "New features are available for {}! It is recommended to update the firmware on your printer."
         ).format(self._name), 0)
     message.addAction(
         "download_config",
         catalog.i18nc("@action:button", "Update Firmware"), "globe",
         catalog.i18nc(
             "@info:tooltip",
             "Automatically download and install the latest firmware"))
     message.actionTriggered.connect(self.beginUpdate)
     message.show()
예제 #19
0
 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()
예제 #20
0
    def _showUnmappedSettingsMessage(self):
        Logger.log("d", "called: {}".format(self._found_unmapped.keys()))

        msg = (
            "Settings for the DuetRRF plugin moved to the Printer preferences.\n\n"
            "Please go to:\n"
            "→ Cura Preferences\n"
            "→ Printers\n"
            "→ activate and select your printer\n"
            "→ click on 'Connect Duet RepRapFirmware'\n")
        if self._found_unmapped:
            msg += "\n\n"
            msg += "You have unmapped settings for unknown printers:\n"
            for printer_id, data in self._found_unmapped.items():
                t = "   {}:\n".format(printer_id)
                if "url" in data and data["url"].strip():
                    t += "→ URL: {}\n".format(data["url"])
                if "duet_password" in data and data["duet_password"].strip():
                    t += "→ Duet password: {}\n".format(data["duet_password"])
                if "http_username" in data and data["http_username"].strip():
                    t += "→ HTTP Basic username: {}\n".format(
                        data["http_username"])
                if "http_password" in data and data["http_password"].strip():
                    t += "→ HTTP Basic password: {}\n".format(
                        data["http_password"])
                msg += t

        message = Message(
            msg,
            lifetime=0,
            title="DuetRRF: Settings moved to Cura Preferences!",
        )
        if self._found_unmapped:
            message.addAction(
                action_id="ignore",
                name=catalog.i18nc("@action:button", "Ignore"),
                icon="",
                description="Close this message",
            )
            message.addAction(
                action_id="delete",
                name=catalog.i18nc("@action:button", "Delete"),
                icon="",
                description="Delete unmapped settings for unknown printers",
            )
            message.actionTriggered.connect(
                self._onActionTriggeredUnmappedSettings)
        message.show()
    def showUpdate(self, newest_version: Version, download_url: str) -> None:
        application_name = Application.getInstance().getApplicationName()
        title_message = i18n_catalog.i18nc("@info:status","{application_name} {version_number} is available!".format(application_name = application_name.title(), version_number = newest_version))
        content_message = i18n_catalog.i18nc("@info:status","{application_name} {version_number} provides a better and more reliable printing experience.".format(application_name = application_name.title(), version_number = newest_version))

        message = Message(text = content_message, title = title_message)
        message.addAction("download", i18n_catalog.i18nc("@action:button", "Download"), "[no_icon]", "[no_description]")

        message.addAction("new_features", i18n_catalog.i18nc("@action:button", "Learn more about the new features"), "[no_icon]", "[no_description]",
                          button_style = Message.ActionButtonStyle.LINK,
                          button_align = Message.ActionButtonStyle.BUTTON_ALIGN_LEFT)

        if self._set_download_url_callback:
            self._set_download_url_callback(download_url)
        message.actionTriggered.connect(self._callback)
        message.show()
예제 #22
0
 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()
예제 #23
0
 def _acceptedRemoveCorruptedPluginMessage(self, plugin_id: str,
                                           original_message: Message):
     message_data = self.uninstallPlugin(plugin_id)
     original_message.hide()
     message = Message(text=message_data["message"],
                       message_type=Message.MessageType.NEUTRAL,
                       lifetime=0)
     message.addAction(
         "dismiss",
         name=i18n_catalog.i18nc("@action:button", "Dismiss"),
         icon="",
         description="Dismiss this message",
         button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
     message.pyQtActionTriggered.connect(
         lambda message, action: message.hide())
     message.show()
예제 #24
0
    def present(self) -> None:
        app_name = self._app.getApplicationDisplayName()

        message = Message(self._i18n_catalog.i18nc(
            "@info:generic",
            "You need to quit and restart {} before changes have effect.", app_name
        ))

        message.addAction("quit",
                          name="Quit " + app_name,
                          icon = "",
                          description="Close the application",
                          button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)

        message.actionTriggered.connect(self._quitClicked)
        message.show()
예제 #25
0
    def _onFinished(self, job):
        if self._stream:
            # Explicitly closing the stream flushes the write-buffer
            try:
                self._stream.close()
                self._stream = None
            except:
                Logger.logException(
                    "w",
                    "An exception occurred while trying to write to removable drive."
                )
                message = Message(catalog.i18nc(
                    "@info:status",
                    "Could not save to removable drive {0}: {1}").format(
                        self.getName(), str(job.getError())),
                                  title=catalog.i18nc("@info:title", "Error"),
                                  message_type=Message.MessageType.ERROR)
                message.show()
                self.writeError.emit(self)
                return

        self._writing = False
        self.writeFinished.emit(self)
        if job.getResult():
            message = Message(catalog.i18nc(
                "@info:status", "Saved to Removable Drive {0} as {1}").format(
                    self.getName(), os.path.basename(job.getFileName())),
                              title=catalog.i18nc("@info:title", "File Saved"),
                              message_type=Message.MessageType.POSITIVE)
            message.addAction(
                "eject", catalog.i18nc("@action:button", "Eject"), "eject",
                catalog.i18nc("@action", "Eject removable device {0}").format(
                    self.getName()))
            message.actionTriggered.connect(self._onActionTriggered)
            message.show()
            self.writeSuccess.emit(self)
        else:
            message = Message(catalog.i18nc(
                "@info:status",
                "Could not save to removable drive {0}: {1}").format(
                    self.getName(), str(job.getError())),
                              title=catalog.i18nc("@info:title", "Error"),
                              message_type=Message.MessageType.ERROR)
            message.show()
            self.writeError.emit(self)
        job.getStream().close()
예제 #26
0
    def _onFinished(self, job):
        if hasattr(job, "_message"):
            job._message.hide()
            job._message = None

        self._writing = False
        self.writeFinished.emit(self)
        if job.getResult():
            message = Message(catalog.i18nc("@info:status", "Saved to Removable Drive {0} as {1}").format(self.getName(), os.path.basename(job.getFileName())))
            message.addAction("eject", catalog.i18nc("@action:button", "Eject"), "eject", catalog.i18nc("@action", "Eject removable device {0}").format(self.getName()))
            message.actionTriggered.connect(self._onActionTriggered)
            message.show()
            self.writeSuccess.emit(self)
        else:
            message = Message(catalog.i18nc("@info:status", "Could not save to removable drive {0}: {1}").format(self.getName(), str(job.getError())))
            message.show()
            self.writeError.emit(self)
        job.getStream().close()
예제 #27
0
    def _showSyncMessage(self) -> None:
        """Show the message if it is not already shown"""

        if self._message is not None:
            self._message.show()
            return

        sync_message = Message(self._i18n_catalog.i18nc(
            "@info:generic",
            "Do you want to sync material and software packages with your account?"),
            title = self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
        sync_message.addAction("sync",
                               name = self._i18n_catalog.i18nc("@action:button", "Sync"),
                               icon = "",
                               description = "Sync your plugins and print profiles to Ultimaker Cura.",
                               button_align = Message.ActionButtonAlignment.ALIGN_RIGHT)
        sync_message.actionTriggered.connect(self._onSyncButtonClicked)
        sync_message.show()
        self._message = sync_message
예제 #28
0
    def _onNetworkFinished(self, reply):
        self.setConnectionState(ConnectionState.connected)
        if reply.operation() == self._qnam.PostOperation:
            Logger.log("i", "_onNetworkFinished reply: %s",
                       repr(reply.readAll()))
            Logger.log("i", "_onNetworkFinished reply.error(): %s",
                       repr(reply.error()))

            self._stage = OutputStage.ready
            if self._message:
                self._message.hide()
            self._message = None

            self.writeFinished.emit(self)
            if reply.error():
                message = Message(
                    catalog.i18nc("@info:status",
                                  "Could not save to {0}: {1}").format(
                                      self.getName(),
                                      str(reply.errorString())))
                message.show()
                self.writeError.emit(self)
            else:
                message = Message(
                    catalog.i18nc("@info:status",
                                  "Saved to {0} as {1}").format(
                                      self.getName(),
                                      os.path.basename(self._fileName)))
                message.addAction(
                    "open_browser",
                    catalog.i18nc("@action:button", "Open Browser"), "globe",
                    catalog.i18nc("@info:tooltip", "Open browser to printer."))
                message.actionTriggered.connect(self._onMessageActionTriggered)
                message.show()
                self.writeSuccess.emit(self)
            self._cleanupRequest()
        elif "status" in reply.url().toString():
            config = ConfigParser(allow_no_value=True)
            ini_string = str(reply.readAll(),
                             'utf8')  # convert qbytearray to str
            config.readfp(io.StringIO(ini_string))
            Logger.log("d", config.get("Status", "lineLastReceived"))
            Logger.log("d", config.get("Status", "jobName"))
예제 #29
0
    def _onFinished(self, job):
        if self._stream:
            # Explicitly closing the stream flushes the write-buffer
            self._stream.close()
            self._stream = None

        self._writing = False
        self.writeFinished.emit(self)
        if job.getResult():
            message = Message(catalog.i18nc("@info:status", "Saved to Removable Drive {0} as {1}").format(self.getName(), os.path.basename(job.getFileName())))
            message.addAction("eject", catalog.i18nc("@action:button", "Eject"), "eject", catalog.i18nc("@action", "Eject removable device {0}").format(self.getName()))
            message.actionTriggered.connect(self._onActionTriggered)
            message.show()
            self.writeSuccess.emit(self)
        else:
            message = Message(catalog.i18nc("@info:status", "Could not save to removable drive {0}: {1}").format(self.getName(), str(job.getError())))
            message.show()
            self.writeError.emit(self)
        job.getStream().close()
예제 #30
0
    def _onWriteJobFinished(self, job):
        self._writing = False
        self.writeFinished.emit(self)
        if job.getResult():
            self.writeSuccess.emit(self)
            message = Message(catalog.i18nc(
                "@info:status Don't translate the XML tags <filename>!",
                "Saved to <filename>{0}</filename>").format(job.getFileName()),
                              title=catalog.i18nc("@info:title", "File Saved"),
                              message_type=Message.MessageType.POSITIVE)
            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 Don't translate the XML tags <filename> or <message>!",
                "Could not save to <filename>{0}</filename>: <message>{1}</message>"
            ).format(job.getFileName(), str(job.getError())),
                              lifetime=0,
                              title=catalog.i18nc("@info:title", "Error"),
                              message_type=Message.MessageType.ERROR)
            message.show()
            self.writeError.emit(self)

        try:
            job.getStream().close()
        except (
                OSError, PermissionError
        ):  #When you don't have the rights to do the final flush or the disk is full.
            message = Message(catalog.i18nc(
                "@info:status",
                "Something went wrong saving to <filename>{0}</filename>: <message>{1}</message>"
            ).format(job.getFileName(), str(job.getError())),
                              title=catalog.i18nc("@info:title", "Error"),
                              message_type=Message.MessageType.ERROR)
            message.show()
            self.writeError.emit(self)
예제 #31
0
class MaterialManager(QObject):
    ##  Creates the global values for the material manager to use.
    def __init__(self, parent = None):
        super().__init__(parent)

        #Material diameter changed warning message.
        self._material_diameter_warning_message = Message(catalog.i18nc("@info:status Has a cancel button next to it.",
            "The selected material diameter causes the material to become incompatible with the current printer."), title = catalog.i18nc("@info:title", "Incompatible Material"))
        self._material_diameter_warning_message.addAction("Undo", catalog.i18nc("@action:button", "Undo"), None, catalog.i18nc("@action", "Undo changing the material diameter."))
        self._material_diameter_warning_message.actionTriggered.connect(self._materialWarningMessageAction)

    ##  Creates an instance of the MaterialManager.
    #
    #   This should only be called by PyQt to create the singleton instance of
    #   this class.
    @staticmethod
    def createMaterialManager(engine = None, script_engine = None):
        return MaterialManager()

    @pyqtSlot(str, str)
    def showMaterialWarningMessage(self, material_id, previous_diameter):
        self._material_diameter_warning_message.previous_diameter = previous_diameter #Make sure that the undo button can properly undo the action.
        self._material_diameter_warning_message.material_id = material_id
        self._material_diameter_warning_message.show()

    ##  Called when clicking "undo" on the warning dialogue for disappeared
    #   materials.
    #
    #   This executes the undo action, restoring the material diameter.
    #
    #   \param button The identifier of the button that was pressed.
    def _materialWarningMessageAction(self, message, button):
        if button == "Undo":
            container_manager = ContainerManager.getInstance()
            container_manager.setContainerMetaDataEntry(self._material_diameter_warning_message.material_id, "properties/diameter", self._material_diameter_warning_message.previous_diameter)
            approximate_previous_diameter = str(round(float(self._material_diameter_warning_message.previous_diameter)))
            container_manager.setContainerMetaDataEntry(self._material_diameter_warning_message.material_id, "approximate_diameter", approximate_previous_diameter)
            container_manager.setContainerProperty(self._material_diameter_warning_message.material_id, "material_diameter", "value", self._material_diameter_warning_message.previous_diameter);
            message.hide()
        else:
            Logger.log("w", "Unknown button action for material diameter warning message: {action}".format(action = button))
    def _onNetworkFinished(self, reply):
        Logger.log("i", "_onNetworkFinished reply: %s", repr(reply.readAll()))
        Logger.log("i", "_onNetworkFinished reply.error(): %s", repr(reply.error()))

        self._stage = OutputStage.ready
        if self._message:
            self._message.hide()
        self._message = None

        self.writeFinished.emit(self)
        if reply.error():
            message = Message(catalog.i18nc("@info:status", "Could not save to {0}: {1}").format(self.getName(), str(reply.errorString())))
            message.show()
            self.writeError.emit(self)
        else:
            message = Message(catalog.i18nc("@info:status", "Saved to {0} as {1}").format(self.getName(), os.path.basename(self._fileName)))
            message.addAction("open_browser", catalog.i18nc("@action:button", "Open Browser"), "globe", catalog.i18nc("@info:tooltip", "Open browser to OctoPrint."))
            message.actionTriggered.connect(self._onMessageActionTriggered)
            message.show()
            self.writeSuccess.emit(self)
        self._cleanupRequest()
예제 #33
0
    def _onWriteJobFinished(self, job):
        self._writing = False
        self.writeFinished.emit(self)
        if job.getResult():
            self.writeSuccess.emit(self)
            message = Message(catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Saved to <filename>{0}</filename>").format(job.getFileName()), title = catalog.i18nc("@info:title", "File Saved"))
            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 Don't translate the XML tags <filename> or <message>!", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(job.getFileName(), str(job.getError())), lifetime = 0, title = catalog.i18nc("@info:title", "Warning"))
            message.show()
            self.writeError.emit(self)

        try:
            job.getStream().close()
        except (OSError, PermissionError): #When you don't have the rights to do the final flush or the disk is full.
            message = Message(catalog.i18nc("@info:status", "Something went wrong saving to <filename>{0}</filename>: <message>{1}</message>").format(job.getFileName(), str(job.getError())), title = catalog.i18nc("@info:title", "Error"))
            message.show()
            self.writeError.emit(self)
예제 #34
0
    def _subscriptionMessages(self, messageCode, prod=None):
        notification_message = Message(lifetime=0)

        if messageCode == self.SubscriptionTypes.trialExpired:
            notification_message.setText(
                i18n_catalog.i18nc("@info:status", "Your free trial has expired! Please subscribe to submit jobs.")
            )
        elif messageCode == self.SubscriptionTypes.subscriptionExpired:
            notification_message.setText(
                i18n_catalog.i18nc("@info:status", "Your subscription has expired! Please renew your subscription to submit jobs.")
            )

        notification_message.addAction(
            action_id="subscribe_link",
            name="<h3><b>Manage Subscription</b></h3>",
            icon="",
            description="Click here to subscribe!",
            button_style=Message.ActionButtonStyle.LINK
        )

        notification_message.actionTriggered.connect(self._openSubscriptionPage)
        notification_message.show()
 def confirmOptimizeModMesh(self):
     self.proxy._optimize_confirmed = False
     msg = Message(
         title="",
         text=
         "Modifier meshes will be removed for the validation.\nDo you want to Continue?",
         #text="Modifier meshes will be removed for the optimization.\nDo you want to Continue?",
         lifetime=0)
     msg.addAction(
         "cancelModMesh",
         i18n_catalog.i18nc("@action", "Cancel"),
         "",  # Icon
         "",  # Description
         button_style=Message.ActionButtonStyle.SECONDARY)
     msg.addAction(
         "continueModMesh",
         i18n_catalog.i18nc("@action", "Continue"),
         "",  # Icon
         ""  # Description
     )
     msg.actionTriggered.connect(self.removeModMeshes)
     msg.show()
예제 #36
0
    def checkQueuedNodes(self):
        for node in self._check_node_queue:
            tri_node = self._toTriMesh(node.getMeshData())
            if tri_node.is_watertight:
                continue

            file_name = node.getMeshData().getFileName()
            base_name = os.path.basename(file_name)

            if file_name in self._mesh_not_watertight_messages:
                self._mesh_not_watertight_messages[file_name].hide()

            message = Message(title=catalog.i18nc("@info:title", "Mesh Tools"))
            body = catalog.i18nc(
                "@info:status",
                "Model %s is not watertight, and may not print properly."
            ) % base_name

            # XRayView may not be available if the plugin has been disabled
            if "XRayView" in self._controller.getAllViews(
            ) and self._controller.getActiveView().getPluginId() != "XRayView":
                body += " " + catalog.i18nc(
                    "@info:status",
                    "Check X-Ray View and repair the model before printing it."
                )
                message.addAction(
                    "X-Ray", catalog.i18nc("@action:button",
                                           "Show X-Ray View"), None, "")
                message.actionTriggered.connect(self._showXRayView)
            else:
                body += " " + catalog.i18nc(
                    "@info:status", "Repair the model before printing it.")

            message.setText(body)
            message.show()

            self._mesh_not_watertight_messages[file_name] = message

        self._check_node_queue = []
예제 #37
0
    def checkNewVersion(self):
        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)
            return

        try:
            reader = codecs.getreader("utf-8")
            data = json.load(reader(latest_version_file))
            try:
                local_version = list(map(int, Application.getInstance().getVersion().split(".")))
            except ValueError:
                Logger.log("w", "Could not determine application version from string %s, not checking for updates", Application.getInstance().getVersion())
                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 = [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.i18n("A new version is available!"))
                                message.addAction("download", "Download", "[no_icon]", "[no_description]")
                                self._url = value["url"]
                                message.actionTriggered.connect(self.actionTriggered)
                                message.show()
                                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))
예제 #38
0
    def openInBlender(self, command):
        """Executes the given command. Asks for closing all other instances of blender.

        !!! Caution !!!
        Terminates all instances of blender without saving. Potential loss of data.

        :param command: The command to open the file in blender.
        """

        self._command = command

        # Checks warn before closing other blender instances flag in settings file.
        if Application.getInstance().getPreferences().getValue(
                'cura_blender/warn_before_closing_other_blender_instances'):
            message = Message(text=catalog.i18nc(
                '@info',
                'This will close all other instances of blender without saving.\nPotential loss of data.'
            ),
                              title=catalog.i18nc('@info:title', 'Caution!'))
            message.addAction(
                'Continue',
                catalog.i18nc('@action:button', 'Continue'),
                '[no_icon]',
                '[no_description]',
                button_align=Message.ActionButtonAlignment.ALIGN_LEFT)
            message.addAction(
                'Ignore',
                catalog.i18nc('@action:button',
                              "Don't show this message again"),
                '[no_icon]',
                '[no_description]',
                button_style=Message.ActionButtonStyle.SECONDARY,
                button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
            message.actionTriggered.connect(
                self._closeAllBlenderInstancesTrigger)
            message.show()
        else:
            self._closeAllBlenderInstances()
예제 #39
0
 def _checkCompatibilities(self, json_data) -> None:
     user_subscribed_packages = [
         plugin["package_id"] for plugin in json_data
     ]
     user_installed_packages = self._package_manager.getUserInstalledPackages(
     )
     user_dismissed_packages = self._package_manager.getDismissedPackages()
     if user_dismissed_packages:
         user_installed_packages += user_dismissed_packages
     # We check if there are packages installed in Cloud Marketplace but not in Cura marketplace
     package_discrepancy = list(
         set(user_subscribed_packages).difference(user_installed_packages))
     if package_discrepancy:
         self._models["subscribed_packages"].addDiscrepancies(
             package_discrepancy)
         self._models["subscribed_packages"].initialize()
         Logger.debug(
             "Discrepancy found between Cloud subscribed packages and Cura installed packages"
         )
         sync_message = Message(
             i18n_catalog.i18nc(
                 "@info:generic",
                 "\nDo you want to sync material and software packages with your account?"
             ),
             lifetime=0,
             title=i18n_catalog.i18nc(
                 "@info:title",
                 "Changes detected from your Ultimaker account",
             ))
         sync_message.addAction(
             "sync",
             name=i18n_catalog.i18nc("@action:button", "Sync"),
             icon="",
             description=
             "Sync your Cloud subscribed packages to your local environment.",
             button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
         sync_message.actionTriggered.connect(self._onSyncButtonClicked)
         sync_message.show()
예제 #40
0
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
    printJobsChanged = pyqtSignal()
    activePrinterChanged = pyqtSignal()
    activeCameraUrlChanged = pyqtSignal()
    receivedPrintJobsChanged = pyqtSignal()

    # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
    # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
    clusterPrintersChanged = pyqtSignal()

    def __init__(self, device_id, address, properties, parent = None) -> None:
        super().__init__(device_id = device_id, address = address, properties=properties, connection_type = ConnectionType.NetworkConnection, parent = parent)
        self._api_prefix = "/cluster-api/v1/"

        self._number_of_extruders = 2

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

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

        self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/MonitorStage.qml")

        # See comments about this hack with the clusterPrintersChanged signal
        self.printersChanged.connect(self.clusterPrintersChanged)

        self._accepts_commands = True  # type: bool

        # Cluster does not have authentication, so default to authenticated
        self._authentication_state = AuthState.Authenticated

        self._error_message = None  # type: Optional[Message]
        self._write_job_progress_message = None  # type: Optional[Message]
        self._progress_message = None  # type: Optional[Message]

        self._active_printer = None  # type: Optional[PrinterOutputModel]

        self._printer_selection_dialog = None  # type: QObject

        self.setPriority(3)  # Make sure the output device gets selected above local file output
        self.setName(self._id)
        self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
        self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))

        self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network"))

        self._printer_uuid_to_unique_name_mapping = {}  # type: Dict[str, str]

        self._finished_jobs = []  # type: List[UM3PrintJobOutputModel]

        self._cluster_size = int(properties.get(b"cluster_size", 0))  # type: int

        self._latest_reply_handler = None  # type: Optional[QNetworkReply]
        self._sending_job = None

        self._active_camera_url = QUrl()  # type: QUrl

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

        self.sendMaterialProfiles()

        # Formats supported by this application (file types that we can actually write).
        if file_handler:
            file_formats = file_handler.getSupportedFileTypesWrite()
        else:
            file_formats = CuraApplication.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()

        global_stack = CuraApplication.getInstance().getGlobalContainerStack()
        # Create a list from the supported file formats string.
        if not global_stack:
            Logger.log("e", "Missing global stack!")
            return

        machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";")
        machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
        # Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
        if "application/x-ufp" not in machine_file_formats and Version(self.firmwareVersion) >= Version("4.4"):
            machine_file_formats = ["application/x-ufp"] + machine_file_formats

        # Take the intersection between file_formats and machine_file_formats.
        format_by_mimetype = {format["mime_type"]: format for format in file_formats}
        file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats.

        if len(file_formats) == 0:
            Logger.log("e", "There are no file formats available to write with!")
            raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!"))
        preferred_format = file_formats[0]

        # Just take the first file format available.
        if file_handler is not None:
            writer = file_handler.getWriterByMimeType(cast(str, preferred_format["mime_type"]))
        else:
            writer = CuraApplication.getInstance().getMeshFileHandler().getWriterByMimeType(cast(str, preferred_format["mime_type"]))

        if not writer:
            Logger.log("e", "Unexpected error when trying to get the FileWriter")
            return

        # This function pauses with the yield, waiting on instructions on which printer it needs to print with.
        if not writer:
            Logger.log("e", "Missing file or mesh writer!")
            return
        self._sending_job = self._sendPrintJob(writer, preferred_format, nodes)
        if self._sending_job is not None:
            self._sending_job.send(None)  # Start the generator.

            if len(self._printers) > 1:  # We need to ask the user.
                self._spawnPrinterSelectionDialog()
                is_job_sent = True
            else:  # Just immediately continue.
                self._sending_job.send("")  # No specifically selected printer.
                is_job_sent = self._sending_job.send(None)

    def _spawnPrinterSelectionDialog(self):
        if self._printer_selection_dialog is None:
            path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/PrintWindow.qml")
            self._printer_selection_dialog = CuraApplication.getInstance().createQmlComponent(path, {"OutputDevice": self})
        if self._printer_selection_dialog is not None:
            self._printer_selection_dialog.show()

    @pyqtProperty(int, constant=True)
    def clusterSize(self) -> int:
        return self._cluster_size

    ##  Allows the user to choose a printer to print with from the printer
    #   selection dialogue.
    #   \param target_printer The name of the printer to target.
    @pyqtSlot(str)
    def selectPrinter(self, target_printer: str = "") -> None:
        if self._sending_job is not None:
            self._sending_job.send(target_printer)

    @pyqtSlot()
    def cancelPrintSelection(self) -> None:
        self._sending_gcode = False

    ##  Greenlet to send a job to the printer over the network.
    #
    #   This greenlet gets called asynchronously in requestWrite. It is a
    #   greenlet in order to optionally wait for selectPrinter() to select a
    #   printer.
    #   The greenlet yields exactly three times: First time None,
    #   \param writer The file writer to use to create the data.
    #   \param preferred_format A dictionary containing some information about
    #   what format to write to. This is necessary to create the correct buffer
    #   types and file extension and such.
    def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict, nodes: List[SceneNode]):
        Logger.log("i", "Sending print job to printer.")
        if self._sending_gcode:
            self._error_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Sending new jobs (temporarily) blocked, still sending the previous print job."))
            self._error_message.show()
            yield #Wait on the user to select a target printer.
            yield #Wait for the write job to be finished.
            yield False #Return whether this was a success or not.
            yield #Prevent StopIteration.

        self._sending_gcode = True

        target_printer = yield #Potentially wait on the user to select a target printer.

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

        stream = io.BytesIO() # type: Union[io.BytesIO, io.StringIO]# Binary mode.
        if preferred_format["mode"] == FileWriter.OutputMode.TextMode:
            stream = io.StringIO()

        job = WriteFileJob(writer, stream, nodes, preferred_format["mode"])

        self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
                                                   title = i18n_catalog.i18nc("@info:title", "Sending Data"), use_inactivity_timer = False)
        self._write_job_progress_message.show()

        self._dummy_lambdas = (target_printer, preferred_format, stream)
        job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished)

        job.start()

        yield True  # Return that we had success!
        yield  # To prevent having to catch the StopIteration exception.

    def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None:
        if self._write_job_progress_message:
            self._write_job_progress_message.hide()

        self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
                                         title = i18n_catalog.i18nc("@info:title", "Sending Data"))
        self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = None, description = "")
        self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
        self._progress_message.show()
        parts = []

        target_printer, preferred_format, stream = self._dummy_lambdas

        # If a specific printer was selected, it should be printed with that machine.
        if target_printer:
            target_printer = self._printer_uuid_to_unique_name_mapping[target_printer]
            parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain"))

        # Add user name to the print_job
        parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))

        file_name = CuraApplication.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"]

        output = stream.getvalue()  # Either str or bytes depending on the output mode.
        if isinstance(stream, io.StringIO):
            output = cast(str, output).encode("utf-8")
        output = cast(bytes, output)

        parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output))

        self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, on_finished = self._onPostPrintJobFinished, on_progress = self._onUploadPrintJobProgress)

    @pyqtProperty(QObject, notify = activePrinterChanged)
    def activePrinter(self) -> Optional[PrinterOutputModel]:
        return self._active_printer

    @pyqtSlot(QObject)
    def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None:
        if self._active_printer != printer:
            self._active_printer = printer
            self.activePrinterChanged.emit()

    @pyqtProperty(QUrl, notify = activeCameraUrlChanged)
    def activeCameraUrl(self) -> "QUrl":
        return self._active_camera_url

    @pyqtSlot(QUrl)
    def setActiveCameraUrl(self, camera_url: "QUrl") -> None:
        if self._active_camera_url != camera_url:
            self._active_camera_url = camera_url
            self.activeCameraUrlChanged.emit()

    def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None:
        if self._progress_message:
            self._progress_message.hide()
        self._compressing_gcode = False
        self._sending_gcode = False

    def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None:
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()
            if self._progress_message and new_progress > self._progress_message.getProgress():
                self._progress_message.show()  # Ensure that the message is visible.
                self._progress_message.setProgress(bytes_sent / bytes_total * 100)

            # If successfully sent:
            if bytes_sent == bytes_total:
                # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to
                # the monitor tab.
                self._success_message = Message(
                    i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."),
                    lifetime=5, dismissable=True,
                    title=i18n_catalog.i18nc("@info:title", "Data Sent"))
                self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Monitor"), icon=None,
                                                description="")
                self._success_message.actionTriggered.connect(self._successMessageActionTriggered)
                self._success_message.show()
        else:
            if self._progress_message is not None:
                self._progress_message.setProgress(0)
                self._progress_message.hide()

    def _progressMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None:
        if action_id == "Abort":
            Logger.log("d", "User aborted sending print to remote.")
            if self._progress_message is not None:
                self._progress_message.hide()
            self._compressing_gcode = False
            self._sending_gcode = False
            CuraApplication.getInstance().getController().setActiveStage("PrepareStage")

            # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request
            # the "reply" should be disconnected
            if self._latest_reply_handler:
                self._latest_reply_handler.disconnect()
                self._latest_reply_handler = None

    def _successMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None:
        if action_id == "View":
            CuraApplication.getInstance().getController().setActiveStage("MonitorStage")

    @pyqtSlot()
    def openPrintJobControlPanel(self) -> None:
        Logger.log("d", "Opening print job control panel...")
        QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))

    @pyqtSlot()
    def openPrinterControlPanel(self) -> None:
        Logger.log("d", "Opening printer control panel...")
        QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))

    @pyqtProperty("QVariantList", notify = printJobsChanged)
    def printJobs(self)-> List[UM3PrintJobOutputModel]:
        return self._print_jobs

    @pyqtProperty(bool, notify = receivedPrintJobsChanged)
    def receivedPrintJobs(self) -> bool:
        return self._received_print_jobs

    @pyqtProperty("QVariantList", notify = printJobsChanged)
    def queuedPrintJobs(self) -> List[UM3PrintJobOutputModel]:
        return [print_job for print_job in self._print_jobs if print_job.state == "queued" or print_job.state == "error"]

    @pyqtProperty("QVariantList", notify = printJobsChanged)
    def activePrintJobs(self) -> List[UM3PrintJobOutputModel]:
        return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"]

    @pyqtProperty("QVariantList", notify = clusterPrintersChanged)
    def connectedPrintersTypeCount(self) -> List[Dict[str, str]]:
        printer_count = {} # type: Dict[str, int]
        for printer in self._printers:
            if printer.type in printer_count:
                printer_count[printer.type] += 1
            else:
                printer_count[printer.type] = 1
        result = []
        for machine_type in printer_count:
            result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])})
        return result

    @pyqtProperty("QVariantList", notify=clusterPrintersChanged)
    def printers(self):
        return self._printers

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

    @pyqtSlot(int, result = str)
    def getTimeCompleted(self, time_remaining: int) -> str:
        current_time = time()
        datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
        return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)

    @pyqtSlot(int, result = str)
    def getDateCompleted(self, time_remaining: int) -> str:
        current_time = time()
        completed = datetime.fromtimestamp(current_time + time_remaining)
        today = datetime.fromtimestamp(current_time)

        # If finishing date is more than 7 days out, using "Mon Dec 3 at HH:MM" format
        if completed.toordinal() > today.toordinal() + 7:
            return completed.strftime("%a %b ") + "{day}".format(day=completed.day)
        
        # If finishing date is within the next week, use "Monday at HH:MM" format
        elif completed.toordinal() > today.toordinal() + 1:
            return completed.strftime("%a")
        
        # If finishing tomorrow, use "tomorrow at HH:MM" format
        elif completed.toordinal() > today.toordinal():
            return "tomorrow"

        # If finishing today, use "today at HH:MM" format
        else:
            return "today"

    @pyqtSlot(str)
    def sendJobToTop(self, print_job_uuid: str) -> None:
        # This function is part of the output device (and not of the printjob output model) as this type of operation
        # is a modification of the cluster queue and not of the actual job.
        data = "{\"to_position\": 0}"
        self.put("print_jobs/{uuid}/move_to_position".format(uuid = print_job_uuid), data, on_finished=None)

    @pyqtSlot(str)
    def deleteJobFromQueue(self, print_job_uuid: str) -> None:
        # This function is part of the output device (and not of the printjob output model) as this type of operation
        # is a modification of the cluster queue and not of the actual job.
        self.delete("print_jobs/{uuid}".format(uuid = print_job_uuid), on_finished=None)

    @pyqtSlot(str)
    def forceSendJob(self, print_job_uuid: str) -> None:
        data = "{\"force\": true}"
        self.put("print_jobs/{uuid}".format(uuid=print_job_uuid), data, on_finished=None)

    def _printJobStateChanged(self) -> None:
        username = self._getUserName()

        if username is None:
            return  # We only want to show notifications if username is set.

        finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"]

        newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username]
        for job in newly_finished_jobs:
            if job.assignedPrinter:
                job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name))
            else:
                job_completed_text =  i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.".format(job_name = job.name))
            job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
            job_completed_message.show()

        # Ensure UI gets updated
        self.printJobsChanged.emit()

        # Keep a list of all completed jobs so we know if something changed next time.
        self._finished_jobs = finished_jobs

    ##  Called when the connection to the cluster changes.
    def connect(self) -> None:
        super().connect()
        self.sendMaterialProfiles()

    def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None:
        reply_url = reply.url().toString()

        uuid = reply_url[reply_url.find("print_jobs/")+len("print_jobs/"):reply_url.rfind("/preview_image")]

        print_job = findByKey(self._print_jobs, uuid)
        if print_job:
            image = QImage()
            image.loadFromData(reply.readAll())
            print_job.updatePreviewImage(image)

    def _update(self) -> None:
        super()._update()
        self.get("printers/", on_finished = self._onGetPrintersDataFinished)
        self.get("print_jobs/", on_finished = self._onGetPrintJobsFinished)

        for print_job in self._print_jobs:
            if print_job.getPreviewImage() is None:
                self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished)

    def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
        self._received_print_jobs = True
        self.receivedPrintJobsChanged.emit()

        if not checkValidGetReply(reply):
            return

        result = loadJsonFromReply(reply)
        if result is None:
            return

        print_jobs_seen = []
        job_list_changed = False
        for idx, print_job_data in enumerate(result):
            print_job = findByKey(self._print_jobs, print_job_data["uuid"])
            if print_job is None:
                print_job = self._createPrintJobModel(print_job_data)
                job_list_changed = True
            elif not job_list_changed:
                # Check if the order of the jobs has changed since the last check
                if self._print_jobs.index(print_job) != idx:
                    job_list_changed = True

            self._updatePrintJob(print_job, print_job_data)

            if print_job.state != "queued" and print_job.state != "error":  # Print job should be assigned to a printer.
                if print_job.state in ["failed", "finished", "aborted", "none"]:
                    # Print job was already completed, so don't attach it to a printer.
                    printer = None
                else:
                    printer = self._getPrinterByKey(print_job_data["printer_uuid"])
            else:  # The job can "reserve" a printer if some changes are required.
                printer = self._getPrinterByKey(print_job_data["assigned_to"])

            if printer:
                printer.updateActivePrintJob(print_job)

            print_jobs_seen.append(print_job)

        # Check what jobs need to be removed.
        removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen]

        for removed_job in removed_jobs:
            job_list_changed = job_list_changed or self._removeJob(removed_job)

        if job_list_changed:
            # Override the old list with the new list (either because jobs were removed / added or order changed)
            self._print_jobs = print_jobs_seen
            self.printJobsChanged.emit()  # Do a single emit for all print job changes.

    def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None:
        if not checkValidGetReply(reply):
            return

        result = loadJsonFromReply(reply)
        if result is None:
            return

        printer_list_changed = False
        printers_seen = []

        for printer_data in result:
            printer = findByKey(self._printers, printer_data["uuid"])

            if printer is None:
                printer = self._createPrinterModel(printer_data)
                printer_list_changed = True

            printers_seen.append(printer)

            self._updatePrinter(printer, printer_data)

        removed_printers = [printer for printer in self._printers if printer not in printers_seen]
        for printer in removed_printers:
            self._removePrinter(printer)

        if removed_printers or printer_list_changed:
            self.printersChanged.emit()

    def _createPrinterModel(self, data: Dict[str, Any]) -> PrinterOutputModel:
        printer = PrinterOutputModel(output_controller = ClusterUM3PrinterOutputController(self),
                                     number_of_extruders = self._number_of_extruders)
        printer.setCameraUrl(QUrl("http://" + data["ip_address"] + ":8080/?action=stream"))
        self._printers.append(printer)
        return printer

    def _createPrintJobModel(self, data: Dict[str, Any]) -> UM3PrintJobOutputModel:
        print_job = UM3PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
                                        key=data["uuid"], name= data["name"])

        configuration = ConfigurationModel()
        extruders = [ExtruderConfigurationModel(position = idx) for idx in range(0, self._number_of_extruders)]
        for index in range(0, self._number_of_extruders):
            try:
                extruder_data = data["configuration"][index]
            except IndexError:
                continue
            extruder = extruders[int(data["configuration"][index]["extruder_index"])]
            extruder.setHotendID(extruder_data.get("print_core_id", ""))
            extruder.setMaterial(self._createMaterialOutputModel(extruder_data.get("material", {})))

        configuration.setExtruderConfigurations(extruders)
        print_job.updateConfiguration(configuration)
        print_job.setCompatibleMachineFamilies(data.get("compatible_machine_families", []))
        print_job.stateChanged.connect(self._printJobStateChanged)
        return print_job

    def _updatePrintJob(self, print_job: UM3PrintJobOutputModel, data: Dict[str, Any]) -> None:
        print_job.updateTimeTotal(data["time_total"])
        print_job.updateTimeElapsed(data["time_elapsed"])
        impediments_to_printing = data.get("impediments_to_printing", [])
        print_job.updateOwner(data["owner"])

        status_set_by_impediment = False
        for impediment in impediments_to_printing:
            if impediment["severity"] == "UNFIXABLE":
                status_set_by_impediment = True
                print_job.updateState("error")
                break

        if not status_set_by_impediment:
            print_job.updateState(data["status"])

        print_job.updateConfigurationChanges(self._createConfigurationChanges(data["configuration_changes_required"]))

    def _createConfigurationChanges(self, data: List[Dict[str, Any]]) -> List[ConfigurationChangeModel]:
        result = []
        for change in data:
            result.append(ConfigurationChangeModel(type_of_change=change["type_of_change"],
                                                   index=change["index"],
                                                   target_name=change["target_name"],
                                                   origin_name=change["origin_name"]))
        return result

    def _createMaterialOutputModel(self, material_data: Dict[str, Any]) -> "MaterialOutputModel":
        material_manager = CuraApplication.getInstance().getMaterialManager()
        material_group_list = None

        # Avoid crashing if there is no "guid" field in the metadata
        material_guid = material_data.get("guid")
        if material_guid:
            material_group_list = material_manager.getMaterialGroupListByGUID(material_guid)

        # This can happen if the connected machine has no material in one or more extruders (if GUID is empty), or the		
        # material is unknown to Cura, so we should return an "empty" or "unknown" material model.		
        if material_group_list is None:
            material_name = i18n_catalog.i18nc("@label:material", "Empty") if len(material_data.get("guid", "")) == 0 \
                        else i18n_catalog.i18nc("@label:material", "Unknown")
            return MaterialOutputModel(guid = material_data.get("guid", ""),
                                        type = material_data.get("type", ""),
                                        color = material_data.get("color", ""),
                                        brand = material_data.get("brand", ""),
                                        name = material_data.get("name", material_name)
                                        )

        # Sort the material groups by "is_read_only = True" first, and then the name alphabetically.
        read_only_material_group_list = list(filter(lambda x: x.is_read_only, material_group_list))
        non_read_only_material_group_list = list(filter(lambda x: not x.is_read_only, material_group_list))
        material_group = None
        if read_only_material_group_list:
            read_only_material_group_list = sorted(read_only_material_group_list, key = lambda x: x.name)
            material_group = read_only_material_group_list[0]
        elif non_read_only_material_group_list:
            non_read_only_material_group_list = sorted(non_read_only_material_group_list, key = lambda x: x.name)
            material_group = non_read_only_material_group_list[0]

        if material_group:
            container = material_group.root_material_node.getContainer()
            color = container.getMetaDataEntry("color_code")
            brand = container.getMetaDataEntry("brand")
            material_type = container.getMetaDataEntry("material")
            name = container.getName()
        else:
            Logger.log("w",
                       "Unable to find material with guid {guid}. Using data as provided by cluster".format(
                           guid=material_data["guid"]))
            color = material_data["color"]
            brand = material_data["brand"]
            material_type = material_data["material"]
            name = i18n_catalog.i18nc("@label:material", "Empty") if material_data["material"] == "empty" \
                else i18n_catalog.i18nc("@label:material", "Unknown")
        return MaterialOutputModel(guid = material_data["guid"], type = material_type,
                                   brand = brand, color = color, name = name)

    def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None:
        # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
        # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
        self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]

        definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"])
        if not definitions:
            Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"])
            return

        machine_definition = definitions[0]

        printer.updateName(data["friendly_name"])
        printer.updateKey(data["uuid"])
        printer.updateType(data["machine_variant"])

        # Do not store the build plate information that comes from connect if the current printer has not build plate information
        if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False):
            printer.updateBuildplateName(data["build_plate"]["type"])
        if not data["enabled"]:
            printer.updateState("disabled")
        else:
            printer.updateState(data["status"])

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

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

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

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

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

        return True

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

    ##  Sync the material profiles in Cura with the printer.
    #
    #   This gets called when connecting to a printer as well as when sending a
    #   print.
    def sendMaterialProfiles(self) -> None:
        job = SendMaterialJob(device = self)
        job.run()
class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinterOutputDevice):
    printJobsChanged = pyqtSignal()
    printersChanged = pyqtSignal()
    selectedPrinterChanged = pyqtSignal()

    def __init__(self, key, address, properties, api_prefix):
        super().__init__(key, address, properties, api_prefix)
        # Store the address of the master.
        self._master_address = address
        name_property = properties.get(b"name", b"")
        if name_property:
            name = name_property.decode("utf-8")
        else:
            name = key

        self._authentication_state = NetworkPrinterOutputDevice.AuthState.Authenticated  # The printer is always authenticated

        self.setName(name)
        description = i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")
        self.setShortDescription(description)
        self.setDescription(description)

        self._stage = OutputStage.ready
        host_override = os.environ.get("CLUSTER_OVERRIDE_HOST", "")
        if host_override:
            Logger.log(
                "w",
                "Environment variable CLUSTER_OVERRIDE_HOST is set to [%s], cluster hosts are now set to this host",
                host_override)
            self._host = "http://" + host_override
        else:
            self._host = "http://" + address

        # is the same as in NetworkPrinterOutputDevicePlugin
        self._cluster_api_version = "1"
        self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"
        self._api_base_uri = self._host + self._cluster_api_prefix

        self._file_name = None
        self._progress_message = None
        self._request = None
        self._reply = None

        # The main reason to keep the 'multipart' form data on the object
        # is to prevent the Python GC from claiming it too early.
        self._multipart = None

        self._print_view = None
        self._request_job = []

        self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml")
        self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml")

        self._print_jobs = []
        self._print_job_by_printer_uuid = {}
        self._print_job_by_uuid = {} # Print jobs by their own uuid
        self._printers = []
        self._printers_dict = {}  # by unique_name

        self._connected_printers_type_count = []
        self._automatic_printer = {"unique_name": "", "friendly_name": "Automatic"}  # empty unique_name IS automatic selection
        self._selected_printer = self._automatic_printer

        self._cluster_status_update_timer = QTimer()
        self._cluster_status_update_timer.setInterval(5000)
        self._cluster_status_update_timer.setSingleShot(False)
        self._cluster_status_update_timer.timeout.connect(self._requestClusterStatus)

        self._can_pause = True
        self._can_abort = True
        self._can_pre_heat_bed = False
        self._can_control_manually = False
        self._cluster_size = int(properties.get(b"cluster_size", 0))

        self._cleanupRequest()

        #These are texts that are to be translated for future features.
        temporary_translation = i18n_catalog.i18n("This printer is not set up to host a group of connected Ultimaker 3 printers.")
        temporary_translation2 = i18n_catalog.i18nc("Count is number of printers.", "This printer is the host for a group of {count} connected Ultimaker 3 printers.").format(count = 3)
        temporary_translation3 = i18n_catalog.i18n("{printer_name} has finished printing '{job_name}'. Please collect the print and confirm clearing the build plate.") #When finished.
        temporary_translation4 = i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.") #When configuration changed.

    ##  No authentication, so requestAuthentication should do exactly nothing
    @pyqtSlot()
    def requestAuthentication(self, message_id = None, action_id = "Retry"):
        pass    # Cura Connect doesn't do any authorization

    def setAuthenticationState(self, auth_state):
        self._authentication_state = NetworkPrinterOutputDevice.AuthState.Authenticated  # The printer is always authenticated

    def _verifyAuthentication(self):
        pass

    def _checkAuthentication(self):
        Logger.log("d", "_checkAuthentication Cura Connect - nothing to be done")

    @pyqtProperty(QObject, notify=selectedPrinterChanged)
    def controlItem(self):
        # TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
        if not self._control_item:
            self._createControlViewFromQML()
        name = self._selected_printer.get("friendly_name")
        if name == self._automatic_printer.get("friendly_name") or name == "":
            return self._control_item
        # Let cura use the default.
        return None

    @pyqtSlot(int, result = str)
    def getTimeCompleted(self, time_remaining):
        current_time = time.time()
        datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining)
        return "{hour:02d}:{minute:02d}".format(hour = datetime_completed.hour, minute = datetime_completed.minute)

    @pyqtSlot(int, result = str)
    def getDateCompleted(self, time_remaining):
        current_time = time.time()
        datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining)
        return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()

    @pyqtProperty(int, constant = True)
    def clusterSize(self):
        return self._cluster_size

    @pyqtProperty(str, notify=selectedPrinterChanged)
    def name(self):
        # Show the name of the selected printer.
        # This is not the nicest way to do this, but changes to the Cura UI are required otherwise.
        name = self._selected_printer.get("friendly_name")
        if name != self._automatic_printer.get("friendly_name"):
            return name
        # Return name of cluster master.
        return self._properties.get(b"name", b"").decode("utf-8")

    def connect(self):
        super().connect()
        self._cluster_status_update_timer.start()

    def close(self):
        super().close()
        self._cluster_status_update_timer.stop()

    def _setJobState(self, job_state):
        if not self._selected_printer:
            return

        selected_printer_uuid = self._printers_dict[self._selected_printer["unique_name"]]["uuid"]
        if selected_printer_uuid not in self._print_job_by_printer_uuid:
            return

        print_job_uuid = self._print_job_by_printer_uuid[selected_printer_uuid]["uuid"]

        url = QUrl(self._api_base_uri + "print_jobs/" + print_job_uuid + "/action")
        put_request = QNetworkRequest(url)
        put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
        data = '{"action": "' + job_state + '"}'
        self._manager.put(put_request, data.encode())

    def _requestClusterStatus(self):
        # TODO: Handle timeout. We probably want to know if the cluster is still reachable or not.
        url = QUrl(self._api_base_uri + "printers/")
        printers_request = QNetworkRequest(url)
        self._addUserAgentHeader(printers_request)
        self._manager.get(printers_request)
        # See _finishedPrintersRequest()

        if self._printers:  # if printers is not empty
            url = QUrl(self._api_base_uri + "print_jobs/")
            print_jobs_request = QNetworkRequest(url)
            self._addUserAgentHeader(print_jobs_request)
            self._manager.get(print_jobs_request)
            # See _finishedPrintJobsRequest()

    def _finishedPrintJobsRequest(self, reply):
        try:
            json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
        except json.decoder.JSONDecodeError:
            Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
            return
        self.setPrintJobs(json_data)

    def _finishedPrintersRequest(self, reply):
        try:
            json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
        except json.decoder.JSONDecodeError:
            Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
            return
        self.setPrinters(json_data)

    def materialHotendChangedMessage(self, callback):
        # When there is just one printer, the activate configuration option is enabled
        if (self._cluster_size == 1):
            super().materialHotendChangedMessage(callback = callback)

    def _startCameraStream(self):
        ## Request new image
        url = QUrl("http://" + self._printers_dict[self._selected_printer["unique_name"]]["ip_address"] + ":8080/?action=stream")
        self._image_request = QNetworkRequest(url)
        self._addUserAgentHeader(self._image_request)
        self._image_reply = self._manager.get(self._image_request)
        self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)

    def spawnPrintView(self):
        if self._print_view is None:
            path = os.path.join(self._plugin_path, "PrintWindow.qml")
            self._print_view = Application.getInstance().createQmlComponent(path, {"OutputDevice", self})
        if self._print_view is not None:
            self._print_view.show()

    ##  Store job info, show Print view for settings
    def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
        self._selected_printer = self._automatic_printer  # reset to default option
        self._request_job = [nodes, file_name, filter_by_machine, file_handler, kwargs]

        if self._stage != OutputStage.ready:
            if self._error_message:
                self._error_message.hide()
            self._error_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Sending new jobs (temporarily) blocked, still sending the previous print job."))
            self._error_message.show()
            return

        self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer

        if len(self._printers) > 1:
            self.spawnPrintView()  # Ask user how to print it.
        elif len(self._printers) == 1:
            # If there is only one printer, don't bother asking.
            self.selectAutomaticPrinter()
            self.sendPrintJob()
        else:
            # Cluster has no printers, warn the user of this.
            if self._error_message:
                self._error_message.hide()
            self._error_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Unable to send new print job: this 3D printer is not (yet) set up to host a group of connected Ultimaker 3 printers."))
            self._error_message.show()

    ##  Actually send the print job, called from the dialog
    #   :param: require_printer_name: name of printer, or ""
    @pyqtSlot()
    def sendPrintJob(self):
        nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job
        require_printer_name = self._selected_printer["unique_name"]

        self._send_gcode_start = time.time()
        Logger.log("d", "Sending print job [%s] to host..." % file_name)

        if self._stage != OutputStage.ready:
            Logger.log("d", "Unable to send print job as the state is %s", self._stage)
            raise OutputDeviceError.DeviceBusyError()
        self._stage = OutputStage.uploading

        self._file_name = "%s.gcode.gz" % file_name
        self._showProgressMessage()

        new_request = self._buildSendPrintJobHttpRequest(require_printer_name)
        if new_request is None or self._stage != OutputStage.uploading:
            return
        self._request = new_request
        self._reply = self._manager.post(self._request, self._multipart)
        self._reply.uploadProgress.connect(self._onUploadProgress)
        # See _finishedPostPrintJobRequest()

    def _buildSendPrintJobHttpRequest(self, require_printer_name):
        api_url = QUrl(self._api_base_uri + "print_jobs/")
        request = QNetworkRequest(api_url)
        # Create multipart request and add the g-code.
        self._multipart = QHttpMultiPart(QHttpMultiPart.FormDataType)

        # Add gcode
        part = QHttpPart()
        part.setHeader(QNetworkRequest.ContentDispositionHeader,
                       'form-data; name="file"; filename="%s"' % self._file_name)

        gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")
        compressed_gcode = self._compressGcode(gcode)
        if compressed_gcode is None:
            return None     # User aborted print, so stop trying.

        part.setBody(compressed_gcode)
        self._multipart.append(part)

        # require_printer_name "" means automatic
        if require_printer_name:
            self._multipart.append(self.__createKeyValueHttpPart("require_printer_name", require_printer_name))
        user_name = self.__get_username()
        if user_name is None:
            user_name = "unknown"
        self._multipart.append(self.__createKeyValueHttpPart("owner", user_name))

        self._addUserAgentHeader(request)
        return request

    def _compressGcode(self, gcode):
        self._compressing_print = True
        batched_line = ""
        max_chars_per_line = int(1024 * 1024 / 4)  # 1 / 4  MB

        byte_array_file_data = b""

        def _compressDataAndNotifyQt(data_to_append):
            compressed_data = gzip.compress(data_to_append.encode("utf-8"))
            self._progress_message.setProgress(-1)  # Tickle the message so that it's clear that it's still being used.
            QCoreApplication.processEvents()  # Ensure that the GUI does not freeze.
            # Pretend that this is a response, as zipping might take a bit of time.
            self._last_response_time = time.time()
            return compressed_data

        if gcode is None:
            Logger.log("e", "Unable to find sliced gcode, returning empty.")
            return byte_array_file_data

        for line in gcode:
            if not self._compressing_print:
                self._progress_message.hide()
                return None     # Stop trying to zip, abort was called.
            batched_line += line
            # if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file.
            # Compressing line by line in this case is extremely slow, so we need to batch them.
            if len(batched_line) < max_chars_per_line:
                continue
            byte_array_file_data += _compressDataAndNotifyQt(batched_line)
            batched_line = ""

        # Also compress the leftovers.
        if batched_line:
            byte_array_file_data += _compressDataAndNotifyQt(batched_line)

        return byte_array_file_data

    def __createKeyValueHttpPart(self, key, value):
        metadata_part = QHttpPart()
        metadata_part.setHeader(QNetworkRequest.ContentTypeHeader, 'text/plain')
        metadata_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="%s"' % (key))
        metadata_part.setBody(bytearray(value, "utf8"))
        return metadata_part

    def __get_username(self):
        try:
            return getpass.getuser()
        except:
            Logger.log("d", "Could not get the system user name, returning 'unknown' instead.")
            return None

    def _finishedPrintJobPostRequest(self, reply):
        self._stage = OutputStage.ready
        if self._progress_message:
            self._progress_message.hide()
        self._progress_message = None
        self.writeFinished.emit(self)

        if reply.error():
            self._showRequestFailedMessage(reply)
            self.writeError.emit(self)
        else:
            self._showRequestSucceededMessage()
            self.writeSuccess.emit(self)

        self._cleanupRequest()

    def _showRequestFailedMessage(self, reply):
        if reply is not None:
            Logger.log("w", "Unable to send print job to group {cluster_name}: {error_string} ({error})".format(
                cluster_name = self.getName(),
                error_string = str(reply.errorString()),
                error = str(reply.error())))
            error_message_template = i18n_catalog.i18nc("@info:status", "Unable to send print job to group {cluster_name}.")
            message = Message(text=error_message_template.format(
                cluster_name = self.getName()))
            message.show()

    def _showRequestSucceededMessage(self):
        confirmation_message_template = i18n_catalog.i18nc(
            "@info:status",
            "Sent {file_name} to group {cluster_name}."
        )
        file_name = os.path.basename(self._file_name).split(".")[0]
        message_text = confirmation_message_template.format(cluster_name = self.getName(), file_name = file_name)
        message = Message(text=message_text)
        button_text = i18n_catalog.i18nc("@action:button", "Show print jobs")
        button_tooltip = i18n_catalog.i18nc("@info:tooltip", "Opens the print jobs interface in your browser.")
        message.addAction("open_browser", button_text, "globe", button_tooltip)
        message.actionTriggered.connect(self._onMessageActionTriggered)
        message.show()

    def setPrintJobs(self, print_jobs):
        #TODO: hack, last seen messes up the check, so drop it.
        for job in print_jobs:
            del job["last_seen"]
            # Strip any extensions
            job["name"] = self._removeGcodeExtension(job["name"])

        if self._print_jobs != print_jobs:
            old_print_jobs = self._print_jobs
            self._print_jobs = print_jobs

            self._notifyFinishedPrintJobs(old_print_jobs, print_jobs)
            self._notifyConfigurationChangeRequired(old_print_jobs, print_jobs)

            # Yes, this is a hacky way of doing it, but it's quick and the API doesn't give the print job per printer
            # for some reason. ugh.
            self._print_job_by_printer_uuid = {}
            self._print_job_by_uuid = {}
            for print_job in print_jobs:
                if "printer_uuid" in print_job and print_job["printer_uuid"] is not None:
                    self._print_job_by_printer_uuid[print_job["printer_uuid"]] = print_job
                self._print_job_by_uuid[print_job["uuid"]] = print_job
            self.printJobsChanged.emit()

    def _removeGcodeExtension(self, name):
        parts = name.split(".")
        if parts[-1].upper() == "GZ":
            parts = parts[:-1]
        if parts[-1].upper() == "GCODE":
            parts = parts[:-1]
        return ".".join(parts)

    def _notifyFinishedPrintJobs(self, old_print_jobs, new_print_jobs):
        """Notify the user when any of their print jobs have just completed.

        Arguments:

        old_print_jobs -- the previous list of print job status information as returned by the cluster REST API.
        new_print_jobs -- the current list of print job status information as returned by the cluster REST API.
        """
        if old_print_jobs is None:
            return

        username = self.__get_username()
        if username is None:
            return

        our_old_print_jobs = self.__filterOurPrintJobs(old_print_jobs)
        our_old_not_finished_print_jobs = [pj for pj in our_old_print_jobs if pj["status"] != "wait_cleanup"]

        our_new_print_jobs = self.__filterOurPrintJobs(new_print_jobs)
        our_new_finished_print_jobs = [pj for pj in our_new_print_jobs if pj["status"] == "wait_cleanup"]

        old_not_finished_print_job_uuids = set([pj["uuid"] for pj in our_old_not_finished_print_jobs])

        for print_job in our_new_finished_print_jobs:
            if print_job["uuid"] in old_not_finished_print_job_uuids:

                printer_name = self.__getPrinterNameFromUuid(print_job["printer_uuid"])
                if printer_name is None:
                    printer_name = i18n_catalog.i18nc("@label Printer name", "Unknown")

                message_text = (i18n_catalog.i18nc("@info:status",
                                "Printer '{printer_name}' has finished printing '{job_name}'.")
                                .format(printer_name=printer_name, job_name=print_job["name"]))
                message = Message(text=message_text, title=i18n_catalog.i18nc("@info:status", "Print finished"))
                Application.getInstance().showMessage(message)
                Application.getInstance().showToastMessage(
                    i18n_catalog.i18nc("@info:status", "Print finished"),
                    message_text)

    def __filterOurPrintJobs(self, print_jobs):
        username = self.__get_username()
        return [print_job for print_job in print_jobs if print_job["owner"] == username]

    def _notifyConfigurationChangeRequired(self, old_print_jobs, new_print_jobs):
        if old_print_jobs is None:
            return

        old_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(old_print_jobs))
        new_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(new_print_jobs))
        old_change_required_print_job_uuids = set([pj["uuid"] for pj in old_change_required_print_jobs])

        for print_job in new_change_required_print_jobs:
            if print_job["uuid"] not in old_change_required_print_job_uuids:

                printer_name = self.__getPrinterNameFromUuid(print_job["assigned_to"])
                if printer_name is None:
                    # don't report on yet unknown printers
                    continue

                message_text = (i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.")
                                .format(printer_name=printer_name, job_name=print_job["name"]))
                message = Message(text=message_text, title=i18n_catalog.i18nc("@label:status", "Action required"))
                Application.getInstance().showMessage(message)
                Application.getInstance().showToastMessage(
                    i18n_catalog.i18nc("@label:status", "Action required"),
                    message_text)

    def __filterConfigChangePrintJobs(self, print_jobs):
        return filter(self.__isConfigurationChangeRequiredPrintJob, print_jobs)

    def __isConfigurationChangeRequiredPrintJob(self, print_job):
        if print_job["status"] == "queued":
            changes_required = print_job.get("configuration_changes_required", [])
            return len(changes_required) != 0
        return False

    def __getPrinterNameFromUuid(self, printer_uuid):
        for printer in self._printers:
            if printer["uuid"] == printer_uuid:
                return printer["friendly_name"]
        return None

    def setPrinters(self, printers):
        if self._printers != printers:
            self._connected_printers_type_count = []
            printers_count = {}
            self._printers = printers
            self._printers_dict = dict((p["unique_name"], p) for p in printers)  # for easy lookup by unique_name

            for printer in printers:
                variant = printer["machine_variant"]
                if variant in printers_count:
                    printers_count[variant] += 1
                else:
                    printers_count[variant] = 1
            for type in printers_count:
                self._connected_printers_type_count.append({"machine_type": type, "count": printers_count[type]})
            self.printersChanged.emit()

    @pyqtProperty("QVariantList", notify=printersChanged)
    def connectedPrintersTypeCount(self):
        return self._connected_printers_type_count

    @pyqtProperty("QVariantList", notify=printersChanged)
    def connectedPrinters(self):
        return self._printers

    @pyqtProperty(int, notify=printJobsChanged)
    def numJobsPrinting(self):
        num_jobs_printing = 0
        for job in self._print_jobs:
            if job["status"] in ["printing", "wait_cleanup", "sent_to_printer", "pre_print", "post_print"]:
                num_jobs_printing += 1
        return num_jobs_printing

    @pyqtProperty(int, notify=printJobsChanged)
    def numJobsQueued(self):
        num_jobs_queued = 0
        for job in self._print_jobs:
            if job["status"] == "queued":
                num_jobs_queued += 1
        return num_jobs_queued

    @pyqtProperty("QVariantMap", notify=printJobsChanged)
    def printJobsByUUID(self):
        return self._print_job_by_uuid

    @pyqtProperty("QVariantMap", notify=printJobsChanged)
    def printJobsByPrinterUUID(self):
        return self._print_job_by_printer_uuid

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def printJobs(self):
        return self._print_jobs

    @pyqtProperty("QVariantList", notify=printersChanged)
    def printers(self):
        return [self._automatic_printer, ] + self._printers

    @pyqtSlot(str, str)
    def selectPrinter(self, unique_name, friendly_name):
        self.stopCamera()
        self._selected_printer = {"unique_name": unique_name, "friendly_name": friendly_name}
        Logger.log("d", "Selected printer: %s %s", friendly_name, unique_name)
        # TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
        if unique_name == "":
            self._address = self._master_address
        else:
            self._address = self._printers_dict[self._selected_printer["unique_name"]]["ip_address"]

        self.selectedPrinterChanged.emit()

    def _updateJobState(self, job_state):
        name = self._selected_printer.get("friendly_name")
        if name == "" or name == "Automatic":
            # TODO: This is now a bit hacked; If no printer is selected, don't show job state.
            if self._job_state != "":
                self._job_state = ""
                self.jobStateChanged.emit()
        else:
            if self._job_state != job_state:
                self._job_state = job_state
                self.jobStateChanged.emit()

    @pyqtSlot()
    def selectAutomaticPrinter(self):
        self.stopCamera()
        self._selected_printer = self._automatic_printer
        self.selectedPrinterChanged.emit()

    @pyqtProperty("QVariant", notify=selectedPrinterChanged)
    def selectedPrinterName(self):
        return self._selected_printer.get("unique_name", "")

    def getPrintJobsUrl(self):
        return self._host + "/print_jobs"

    def getPrintersUrl(self):
        return self._host + "/printers"

    def _showProgressMessage(self):
        progress_message_template = i18n_catalog.i18nc("@info:progress",
                                               "Sending <filename>{file_name}</filename> to group {cluster_name}")
        file_name = os.path.basename(self._file_name).split(".")[0]
        self._progress_message = Message(progress_message_template.format(file_name = file_name, cluster_name = self.getName()), 0, False, -1)
        self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
        self._progress_message.actionTriggered.connect(self._onMessageActionTriggered)
        self._progress_message.show()

    def _addUserAgentHeader(self, request):
        request.setRawHeader(b"User-agent", b"CuraPrintClusterOutputDevice Plugin")

    def _cleanupRequest(self):
        self._request = None
        self._stage = OutputStage.ready
        self._file_name = None

    def _onFinished(self, reply):
        super()._onFinished(reply)
        reply_url = reply.url().toString()
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if status_code == 500:
            Logger.log("w", "Request to {url} returned a 500.".format(url = reply_url))
            return
        if reply.error() == QNetworkReply.ContentOperationNotPermittedError:
            # It was probably "/api/v1/materials" for legacy UM3
            return
        if reply.error() == QNetworkReply.ContentNotFoundError:
            # It was probably "/api/v1/print_job" for legacy UM3
            return

        if reply.operation() == QNetworkAccessManager.PostOperation:
            if self._cluster_api_prefix + "print_jobs" in reply_url:
                self._finishedPrintJobPostRequest(reply)
                return

        # We need to do this check *after* we process the post operation!
        # If the sending of g-code is cancelled by the user it will result in an error, but we do need to handle this.
        if reply.error() != QNetworkReply.NoError:
            Logger.log("e", "After requesting [%s] we got a network error [%s]. Not processing anything...", reply_url, reply.error())
            return

        elif reply.operation() == QNetworkAccessManager.GetOperation:
            if self._cluster_api_prefix + "print_jobs" in reply_url:
                self._finishedPrintJobsRequest(reply)
            elif self._cluster_api_prefix + "printers" in reply_url:
                self._finishedPrintersRequest(reply)

    @pyqtSlot()
    def openPrintJobControlPanel(self):
        Logger.log("d", "Opening print job control panel...")
        QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl()))

    @pyqtSlot()
    def openPrinterControlPanel(self):
        Logger.log("d", "Opening printer control panel...")
        QDesktopServices.openUrl(QUrl(self.getPrintersUrl()))

    def _onMessageActionTriggered(self, message, action):
        if action == "open_browser":
            QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl()))

        if action == "Abort":
            Logger.log("d", "User aborted sending print to remote.")
            self._progress_message.hide()
            self._compressing_print = False
            if self._reply:
                self._reply.abort()
            self._stage = OutputStage.ready
            Application.getInstance().getController().setActiveStage("PrepareStage")

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

    ##  For cluster below
    def _get_plugin_directory_name(self):
        current_file_absolute_path = os.path.realpath(__file__)
        directory_path = os.path.dirname(current_file_absolute_path)
        _, directory_name = os.path.split(directory_path)
        return directory_name

    @property
    def _plugin_path(self):
        return PluginRegistry.getInstance().getPluginPath(self._get_plugin_directory_name())
예제 #42
0
파일: SliceInfo.py 프로젝트: hroncok/Cura
class SliceInfo(Extension):
    info_url = "https://stats.ultimaker.com/api/cura"

    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 the 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

            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["schema_version"] = 0
            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","")}

            data["extruders"] = []
            extruder_count = len(global_container_stack.extruders)
            extruders = []
            if extruder_count > 1:
                extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
                extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position"))

            if not extruders:
                extruders = [global_container_stack]

            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", "")
                                             }
                extruder_dict["material_used"] = print_information.materialLengths[int(extruder.getMetaDataEntry("position", "0"))]
                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")
                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.printTimesPerFeature
            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()
            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
            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.
예제 #43
0
 def _onWriteJobFinished(self, job):
     message = Message(i18n_catalog.i18nc("Save file completed messsage. {0} is file name", "Saved to {0}".format(job.getFileName())))
     message.addAction("open_folder", i18n_catalog.i18nc("Open Folder message action", "Open Folder"), "open", i18n_catalog.i18n("Open the folder containing the saved file"))
     message._file = job.getFileName()
     message.actionTriggered.connect(self._onMessageActionTriggered)
     message.show()
예제 #44
0
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()
예제 #45
0
파일: SliceInfo.py 프로젝트: ad1217/Cura
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()
예제 #46
0
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
    printJobsChanged = pyqtSignal()
    activePrinterChanged = pyqtSignal()

    # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
    # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
    clusterPrintersChanged = pyqtSignal()

    def __init__(self, device_id, address, properties, parent = None):
        super().__init__(device_id = device_id, address = address, properties=properties, parent = parent)
        self._api_prefix = "/cluster-api/v1/"

        self._number_of_extruders = 2

        self._dummy_lambdas = set()

        self._print_jobs = []

        self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml")
        self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml")

        # See comments about this hack with the clusterPrintersChanged signal
        self.printersChanged.connect(self.clusterPrintersChanged)

        self._accepts_commands = True

        # Cluster does not have authentication, so default to authenticated
        self._authentication_state = AuthState.Authenticated

        self._error_message = None
        self._write_job_progress_message = None
        self._progress_message = None

        self._active_printer = None  # type: Optional[PrinterOutputModel]

        self._printer_selection_dialog = None

        self.setPriority(3)  # Make sure the output device gets selected above local file output
        self.setName(self._id)
        self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
        self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))

        self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network"))

        self._printer_uuid_to_unique_name_mapping = {}

        self._finished_jobs = []

        self._cluster_size = int(properties.get(b"cluster_size", 0))

        self._latest_reply_handler = None

    def requestWrite(self, nodes: List[SceneNode], file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
        self.writeStarted.emit(self)

        #Formats supported by this application (file types that we can actually write).
        if file_handler:
            file_formats = file_handler.getSupportedFileTypesWrite()
        else:
            file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()

        #Create a list from the supported file formats string.
        machine_file_formats = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("file_formats").split(";")
        machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
        #Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
        if "application/x-ufp" not in machine_file_formats and self.printerType == "ultimaker3" and Version(self.firmwareVersion) >= Version("4.4"):
            machine_file_formats = ["application/x-ufp"] + machine_file_formats

        # Take the intersection between file_formats and machine_file_formats.
        format_by_mimetype = {format["mime_type"]: format for format in file_formats}
        file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats.

        if len(file_formats) == 0:
            Logger.log("e", "There are no file formats available to write with!")
            raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!"))
        preferred_format = file_formats[0]

        #Just take the first file format available.
        if file_handler is not None:
            writer = file_handler.getWriterByMimeType(preferred_format["mime_type"])
        else:
            writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(preferred_format["mime_type"])

        #This function pauses with the yield, waiting on instructions on which printer it needs to print with.
        self._sending_job = self._sendPrintJob(writer, preferred_format, nodes)
        self._sending_job.send(None) #Start the generator.

        if len(self._printers) > 1: #We need to ask the user.
            self._spawnPrinterSelectionDialog()
            is_job_sent = True
        else: #Just immediately continue.
            self._sending_job.send("") #No specifically selected printer.
            is_job_sent = self._sending_job.send(None)

    def _spawnPrinterSelectionDialog(self):
        if self._printer_selection_dialog is None:
            path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml")
            self._printer_selection_dialog = Application.getInstance().createQmlComponent(path, {"OutputDevice": self})
        if self._printer_selection_dialog is not None:
            self._printer_selection_dialog.show()

    @pyqtProperty(int, constant=True)
    def clusterSize(self):
        return self._cluster_size

    ##  Allows the user to choose a printer to print with from the printer
    #   selection dialogue.
    #   \param target_printer The name of the printer to target.
    @pyqtSlot(str)
    def selectPrinter(self, target_printer: str = "") -> None:
        self._sending_job.send(target_printer)

    ##  Greenlet to send a job to the printer over the network.
    #
    #   This greenlet gets called asynchronously in requestWrite. It is a
    #   greenlet in order to optionally wait for selectPrinter() to select a
    #   printer.
    #   The greenlet yields exactly three times: First time None,
    #   \param writer The file writer to use to create the data.
    #   \param preferred_format A dictionary containing some information about
    #   what format to write to. This is necessary to create the correct buffer
    #   types and file extension and such.
    def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict, nodes: List[SceneNode]):
        Logger.log("i", "Sending print job to printer.")
        if self._sending_gcode:
            self._error_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Sending new jobs (temporarily) blocked, still sending the previous print job."))
            self._error_message.show()
            yield #Wait on the user to select a target printer.
            yield #Wait for the write job to be finished.
            yield False #Return whether this was a success or not.
            yield #Prevent StopIteration.

        self._sending_gcode = True

        target_printer = yield #Potentially wait on the user to select a target printer.

        # Using buffering greatly reduces the write time for many lines of gcode
        if preferred_format["mode"] == FileWriter.OutputMode.TextMode:
            stream = io.StringIO()
        else: #Binary mode.
            stream = io.BytesIO()

        job = WriteFileJob(writer, stream, nodes, preferred_format["mode"])

        self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
                                                   title = i18n_catalog.i18nc("@info:title", "Sending Data"), use_inactivity_timer = False)
        self._write_job_progress_message.show()

        self._dummy_lambdas = (target_printer, preferred_format, stream)
        job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished)

        job.start()

        yield True #Return that we had success!
        yield #To prevent having to catch the StopIteration exception.

    from cura.Utils.Threading import call_on_qt_thread

    def _sendPrintJobWaitOnWriteJobFinished(self, job):
        self._write_job_progress_message.hide()

        self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
                                         title = i18n_catalog.i18nc("@info:title", "Sending Data"))
        self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = None, description = "")
        self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
        self._progress_message.show()

        parts = []

        target_printer, preferred_format, stream = self._dummy_lambdas

        # If a specific printer was selected, it should be printed with that machine.
        if target_printer:
            target_printer = self._printer_uuid_to_unique_name_mapping[target_printer]
            parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain"))

        # Add user name to the print_job
        parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))

        file_name = Application.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"]

        output = stream.getvalue() #Either str or bytes depending on the output mode.
        if isinstance(stream, io.StringIO):
            output = output.encode("utf-8")

        parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output))

        self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)

    @pyqtProperty(QObject, notify=activePrinterChanged)
    def activePrinter(self) -> Optional[PrinterOutputModel]:
        return self._active_printer

    @pyqtSlot(QObject)
    def setActivePrinter(self, printer: Optional[PrinterOutputModel]):
        if self._active_printer != printer:
            if self._active_printer and self._active_printer.camera:
                self._active_printer.camera.stop()
            self._active_printer = printer
            self.activePrinterChanged.emit()

    def _onPostPrintJobFinished(self, reply):
        self._progress_message.hide()
        self._compressing_gcode = False
        self._sending_gcode = False

    def _onUploadPrintJobProgress(self, bytes_sent:int, bytes_total:int):
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()
            if new_progress > self._progress_message.getProgress():
                self._progress_message.show()  # Ensure that the message is visible.
                self._progress_message.setProgress(bytes_sent / bytes_total * 100)

            # If successfully sent:
            if bytes_sent == bytes_total:
                # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to the
                # monitor tab.
                self._success_message = Message(
                    i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."),
                    lifetime=5, dismissable=True,
                    title=i18n_catalog.i18nc("@info:title", "Data Sent"))
                self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Monitor"), icon=None,
                                                description="")
                self._success_message.actionTriggered.connect(self._successMessageActionTriggered)
                self._success_message.show()
        else:
            self._progress_message.setProgress(0)
            self._progress_message.hide()

    def _progressMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None:
        if action_id == "Abort":
            Logger.log("d", "User aborted sending print to remote.")
            self._progress_message.hide()
            self._compressing_gcode = False
            self._sending_gcode = False
            Application.getInstance().getController().setActiveStage("PrepareStage")

            # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request
            # the "reply" should be disconnected
            if self._latest_reply_handler:
                self._latest_reply_handler.disconnect()
                self._latest_reply_handler = None

    def _successMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None:
        if action_id == "View":
            Application.getInstance().getController().setActiveStage("MonitorStage")

    @pyqtSlot()
    def openPrintJobControlPanel(self) -> None:
        Logger.log("d", "Opening print job control panel...")
        QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))

    @pyqtSlot()
    def openPrinterControlPanel(self) -> None:
        Logger.log("d", "Opening printer control panel...")
        QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def printJobs(self)-> List[PrintJobOutputModel] :
        return self._print_jobs

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def queuedPrintJobs(self) -> List[PrintJobOutputModel]:
        return [print_job for print_job in self._print_jobs if print_job.state == "queued"]

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def activePrintJobs(self) -> List[PrintJobOutputModel]:
        return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"]

    @pyqtProperty("QVariantList", notify=clusterPrintersChanged)
    def connectedPrintersTypeCount(self) -> List[PrinterOutputModel]:
        printer_count = {}
        for printer in self._printers:
            if printer.type in printer_count:
                printer_count[printer.type] += 1
            else:
                printer_count[printer.type] = 1
        result = []
        for machine_type in printer_count:
            result.append({"machine_type": machine_type, "count": printer_count[machine_type]})
        return result

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

    @pyqtSlot(int, result=str)
    def getTimeCompleted(self, time_remaining: int) -> str:
        current_time = time()
        datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
        return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)

    @pyqtSlot(int, result=str)
    def getDateCompleted(self, time_remaining: int) -> str:
        current_time = time()
        datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
        return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()

    def _printJobStateChanged(self) -> None:
        username = self._getUserName()

        if username is None:
            return  # We only want to show notifications if username is set.

        finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"]

        newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username]
        for job in newly_finished_jobs:
            if job.assignedPrinter:
                job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name))
            else:
                job_completed_text =  i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.".format(job_name = job.name))
            job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
            job_completed_message.show()

        # Ensure UI gets updated
        self.printJobsChanged.emit()

        # Keep a list of all completed jobs so we know if something changed next time.
        self._finished_jobs = finished_jobs

    def _update(self) -> None:
        if not super()._update():
            return
        self.get("printers/", onFinished=self._onGetPrintersDataFinished)
        self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)

    def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
        if not checkValidGetReply(reply):
            return

        result = loadJsonFromReply(reply)
        if result is None:
            return

        print_jobs_seen = []
        job_list_changed = False
        for print_job_data in result:
            print_job = findByKey(self._print_jobs, print_job_data["uuid"])

            if print_job is None:
                print_job = self._createPrintJobModel(print_job_data)
                job_list_changed = True

            self._updatePrintJob(print_job, print_job_data)

            if print_job.state != "queued":  # Print job should be assigned to a printer.
                if print_job.state in ["failed", "finished", "aborted"]:
                    # Print job was already completed, so don't attach it to a printer.
                    printer = None
                else:
                    printer = self._getPrinterByKey(print_job_data["printer_uuid"])
            else:  # The job can "reserve" a printer if some changes are required.
                printer = self._getPrinterByKey(print_job_data["assigned_to"])

            if printer:
                printer.updateActivePrintJob(print_job)

            print_jobs_seen.append(print_job)

        # Check what jobs need to be removed.
        removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen]

        for removed_job in removed_jobs:
            job_list_changed |= self._removeJob(removed_job)

        if job_list_changed:
            self.printJobsChanged.emit()  # Do a single emit for all print job changes.

    def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None:
        if not checkValidGetReply(reply):
            return

        result = loadJsonFromReply(reply)
        if result is None:
            return

        printer_list_changed = False
        printers_seen = []

        for printer_data in result:
            printer = findByKey(self._printers, printer_data["uuid"])

            if printer is None:
                printer = self._createPrinterModel(printer_data)
                printer_list_changed = True

            printers_seen.append(printer)

            self._updatePrinter(printer, printer_data)

        removed_printers = [printer for printer in self._printers if printer not in printers_seen]
        for printer in removed_printers:
            self._removePrinter(printer)

        if removed_printers or printer_list_changed:
            self.printersChanged.emit()

    def _createPrinterModel(self, data: Dict) -> PrinterOutputModel:
        printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
                                     number_of_extruders=self._number_of_extruders)
        printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream"))
        self._printers.append(printer)
        return printer

    def _createPrintJobModel(self, data: Dict) -> PrintJobOutputModel:
        print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
                                        key=data["uuid"], name= data["name"])
        print_job.stateChanged.connect(self._printJobStateChanged)
        self._print_jobs.append(print_job)
        return print_job

    def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict) -> None:
        print_job.updateTimeTotal(data["time_total"])
        print_job.updateTimeElapsed(data["time_elapsed"])
        print_job.updateState(data["status"])
        print_job.updateOwner(data["owner"])

    def _updatePrinter(self, printer: PrinterOutputModel, data: Dict) -> None:
        # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
        # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
        self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]

        definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"])
        if not definitions:
            Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"])
            return

        machine_definition = definitions[0]

        printer.updateName(data["friendly_name"])
        printer.updateKey(data["uuid"])
        printer.updateType(data["machine_variant"])

        # Do not store the buildplate information that comes from connect if the current printer has not buildplate information
        if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False):
            printer.updateBuildplateName(data["build_plate"]["type"])
        if not data["enabled"]:
            printer.updateState("disabled")
        else:
            printer.updateState(data["status"])

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

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

            material_data = extruder_data["material"]
            if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
                containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
                                                                                    GUID=material_data["guid"])
                if containers:
                    color = containers[0].getMetaDataEntry("color_code")
                    brand = containers[0].getMetaDataEntry("brand")
                    material_type = containers[0].getMetaDataEntry("material")
                    name = containers[0].getName()
                else:
                    Logger.log("w",
                               "Unable to find material with guid {guid}. Using data as provided by cluster".format(
                                   guid=material_data["guid"]))
                    color = material_data["color"]
                    brand = material_data["brand"]
                    material_type = material_data["material"]
                    name = "Empty" if material_data["material"] == "empty" else "Unknown"

                material = MaterialOutputModel(guid=material_data["guid"], type=material_type,
                                               brand=brand, color=color, name=name)
                extruder.updateActiveMaterial(material)

    def _removeJob(self, job: PrintJobOutputModel):
        if job not in self._print_jobs:
            return False

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

        return True

    def _removePrinter(self, printer: PrinterOutputModel):
        self._printers.remove(printer)
        if self._active_printer == printer:
            self._active_printer = None
            self.activePrinterChanged.emit()
예제 #47
0
파일: SliceInfo.py 프로젝트: 3d20/Cura
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 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"):
            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()

        # Get total material used (in mm^3)
        print_information = Application.getInstance().getPrintInformation()
        material_radius = 0.5 * global_container_stack.getProperty("material_diameter", "value")
        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(),
            "settings": global_container_stack.serialize(), # global_container with references on used containers
            "version": Application.getInstance().getVersion(),
            "modelhash": "None",
            "printtime": print_information.currentPrintTime.getDisplayString(),
            "filament": material_used,
            "language": Preferences.getInstance().getValue("general/language"),
            "materials_profiles ": {}
        }
        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:
                submitted_data["settings_%s" %(container_id)] = container_serialized # This can be anything, eg. INI, JSON, etc.
            else:
                Logger.log("i", "No data found in %s to be serialized!", container_id)

        # 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(self.info_url, data = binary_data, timeout = 1)
            Logger.log("i", "Sent anonymous slice info to %s", self.info_url)
            f.close()
        except Exception as e:
            Logger.logException("e", e)
예제 #48
0
class SliceInfo(QObject, Extension):
    info_url = "https://stats.ultimaker.com/api/cura"

    def __init__(self, parent = None):
        QObject.__init__(self, parent)
        Extension.__init__(self)

        self._application = Application.getInstance()

        self._application.getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
        self._application.getPreferences().addPreference("info/send_slice_info", True)
        self._application.getPreferences().addPreference("info/asked_send_slice_info", False)

        self._more_info_dialog = None
        self._example_data_content = None

        self._application.initializationFinished.connect(self._onAppInitialized)

    def _onAppInitialized(self):
        # DO NOT read any preferences values in the constructor because at the time plugins are created, no version
        # upgrade has been performed yet because version upgrades are plugins too!
        if not self._application.getPreferences().getValue("info/asked_send_slice_info"):
            self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymized usage statistics."),
                                                   lifetime = 0,
                                                   dismissable = False,
                                                   title = catalog.i18nc("@info:title", "Collecting Data"))

            self.send_slice_info_message.addAction("MoreInfo", name = catalog.i18nc("@action:button", "More info"), icon = None,
                                                   description = catalog.i18nc("@action:tooltip", "See more information on what data Cura sends."), button_style = Message.ActionButtonStyle.LINK)

            self.send_slice_info_message.addAction("Dismiss", name = catalog.i18nc("@action:button", "Allow"), icon = None,
                                                   description = catalog.i18nc("@action:tooltip", "Allow Cura to send anonymized usage statistics to help prioritize future improvements to Cura. Some of your preferences and settings are sent, the Cura version and a hash of the models you're slicing."))
            self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
            self.send_slice_info_message.show()

        if self._more_info_dialog is None:
            self._more_info_dialog = self._createDialog("MoreInfoWindow.qml")

    ##  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):
        Application.getInstance().getPreferences().setValue("info/asked_send_slice_info", True)
        if action_id == "MoreInfo":
            self.showMoreInfoDialog()
        self.send_slice_info_message.hide()

    def showMoreInfoDialog(self):
        if self._more_info_dialog is None:
            self._more_info_dialog = self._createDialog("MoreInfoWindow.qml")
        self._more_info_dialog.open()

    def _createDialog(self, qml_name):
        Logger.log("d", "Creating dialog [%s]", qml_name)
        file_path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name)
        dialog = Application.getInstance().createQmlComponent(file_path, {"manager": self})
        return dialog

    @pyqtSlot(result = str)
    def getExampleData(self) -> Optional[str]:
        if self._example_data_content is None:
            plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
            if not plugin_path:
                Logger.log("e", "Could not get plugin path!", self.getPluginId())
                return None
            file_path = os.path.join(plugin_path, "example_data.json")
            if file_path:
                with open(file_path, "r", encoding = "utf-8") as f:
                    self._example_data_content = f.read()
        return self._example_data_content

    @pyqtSlot(bool)
    def setSendSliceInfo(self, enabled: bool):
        Application.getInstance().getPreferences().setValue("info/send_slice_info", enabled)

    def _getUserModifiedSettingKeys(self) -> list:
        from cura.CuraApplication import CuraApplication
        application = cast(CuraApplication, Application.getInstance())
        machine_manager = application.getMachineManager()
        global_stack = machine_manager.activeMachine

        user_modified_setting_keys = set()  # type: Set[str]

        for stack in [global_stack] + list(global_stack.extruders.values()):
            # Get all settings in user_changes and quality_changes
            all_keys = stack.userChanges.getAllKeys() | stack.qualityChanges.getAllKeys()
            user_modified_setting_keys |= all_keys

        return list(sorted(user_modified_setting_keys))

    def _onWriteStarted(self, output_device):
        try:
            if not Application.getInstance().getPreferences().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

            from cura.CuraApplication import CuraApplication
            application = cast(CuraApplication, Application.getInstance())
            machine_manager = application.getMachineManager()
            print_information = application.getPrintInformation()

            global_stack = machine_manager.activeMachine

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

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

            definition_changes = global_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"] = Application.getInstance().getPreferences().getValue("general/language")
            data["os"] = {"type": platform.system(), "version": platform.version()}

            data["active_machine"] = {"definition_id": global_stack.definition.getId(),
                                      "manufacturer": global_stack.definition.getMetaDataEntry("manufacturer", "")}

            # add extruder specific data to slice info
            data["extruders"] = []
            extruders = list(global_stack.extruders.values())
            extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position"))

            for extruder in extruders:
                extruder_dict = dict()
                extruder_dict["active"] = machine_manager.activeStack == extruder
                extruder_dict["material"] = {"GUID": extruder.material.getMetaData().get("GUID", ""),
                                             "type": extruder.material.getMetaData().get("material", ""),
                                             "brand": extruder.material.getMetaData().get("brand", "")
                                             }
                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")
                extruder_dict["extruder_settings"] = extruder_settings
                data["extruders"].append(extruder_dict)

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

            data["user_modified_setting_keys"] = self._getUserModifiedSettingKeys()

            data["models"] = []
            # Listing all files placed on the build plate
            for node in DepthFirstIterator(application.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.getExtruderPositionValueWithDefault("support_extruder_nr"))

                        # 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()
            print_settings["layer_height"] = global_stack.getProperty("layer_height", "value")

            # Support settings
            print_settings["support_enabled"] = global_stack.getProperty("support_enable", "value")
            print_settings["support_extruder_nr"] = int(global_stack.getExtruderPositionValueWithDefault("support_extruder_nr"))

            # Platform adhesion settings
            print_settings["adhesion_type"] = global_stack.getProperty("adhesion_type", "value")

            # Shell settings
            print_settings["wall_line_count"] = global_stack.getProperty("wall_line_count", "value")
            print_settings["retraction_enable"] = global_stack.getProperty("retraction_enable", "value")

            # Prime tower settings
            print_settings["prime_tower_enable"] = global_stack.getProperty("prime_tower_enable", "value")

            # Infill settings
            print_settings["infill_sparse_density"] = global_stack.getProperty("infill_sparse_density", "value")
            print_settings["infill_pattern"] = global_stack.getProperty("infill_pattern", "value")
            print_settings["gradual_infill_steps"] = global_stack.getProperty("gradual_infill_steps", "value")

            print_settings["print_sequence"] = global_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
            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.
예제 #49
0
파일: SliceInfo.py 프로젝트: cederom/Cura
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 type(node) is not SceneNode or not node.getMeshData():
                    continue
                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.
예제 #50
0
class UM3OutputDevicePlugin(OutputDevicePlugin):
    addDeviceSignal = Signal()
    removeDeviceSignal = Signal()
    discoveredDevicesChanged = Signal()
    cloudFlowIsPossible = Signal()

    def __init__(self):
        super().__init__()
        
        self._zero_conf = None
        self._zero_conf_browser = None

        self._application = CuraApplication.getInstance()

        # Create a cloud output device manager that abstracts all cloud connection logic away.
        self._cloud_output_device_manager = CloudOutputDeviceManager()

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addDeviceSignal.connect(self._onAddDevice)
        self.removeDeviceSignal.connect(self._onRemoveDevice)

        self._application.globalContainerStackChanged.connect(self.reCheckConnections)

        self._discovered_devices = {}
        
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        self._min_cluster_version = Version("4.0.0")
        self._min_cloud_version = Version("5.2.0")

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"
        self._cluster_api_version = "1"
        self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"

        # Get list of manual instances from preferences
        self._preferences = CuraApplication.getInstance().getPreferences()
        self._preferences.addPreference("um3networkprinting/manual_instances",
                                        "")  # A comma-separated list of ip adresses or hostnames

        self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")

        # Store the last manual entry key
        self._last_manual_entry_key = "" # type: str

        # The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests
        # which fail to get detailed service info.
        # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
        # them up and process them.
        self._service_changed_request_queue = Queue()
        self._service_changed_request_event = Event()
        self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True)
        self._service_changed_request_thread.start()

        self._account = self._application.getCuraAPI().account

        # Check if cloud flow is possible when user logs in
        self._account.loginStateChanged.connect(self.checkCloudFlowIsPossible)

        # Check if cloud flow is possible when user switches machines
        self._application.globalContainerStackChanged.connect(self._onMachineSwitched)

        # Listen for when cloud flow is possible 
        self.cloudFlowIsPossible.connect(self._onCloudFlowPossible)

        # Listen if cloud cluster was added
        self._cloud_output_device_manager.addedCloudCluster.connect(self._onCloudPrintingConfigured)

        # Listen if cloud cluster was removed
        self._cloud_output_device_manager.removedCloudCluster.connect(self.checkCloudFlowIsPossible)

        self._start_cloud_flow_message = None # type: Optional[Message]
        self._cloud_flow_complete_message = None # type: Optional[Message]

    def getDiscoveredDevices(self):
        return self._discovered_devices

    def getLastManualDevice(self) -> str:
        return self._last_manual_entry_key

    def resetLastManualDevice(self) -> None:
        self._last_manual_entry_key = ""

    ##  Start looking for devices on network.
    def start(self):
        self.startDiscovery()
        self._cloud_output_device_manager.start()

    def startDiscovery(self):
        self.stop()
        if self._zero_conf_browser:
            self._zero_conf_browser.cancel()
            self._zero_conf_browser = None  # Force the old ServiceBrowser to be destroyed.

        for instance_name in list(self._discovered_devices):
            self._onRemoveDevice(instance_name)

        self._zero_conf = Zeroconf()
        self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.',
                                                 [self._appendServiceChangedRequest])

        # Look for manual instances from preference
        for address in self._manual_instances:
            if address:
                self.addManualDevice(address)
        self.resetLastManualDevice()

    def reCheckConnections(self):
        active_machine = CuraApplication.getInstance().getGlobalContainerStack()
        if not active_machine:
            return

        um_network_key = active_machine.getMetaDataEntry("um_network_key")

        for key in self._discovered_devices:
            if key == um_network_key:
                if not self._discovered_devices[key].isConnected():
                    Logger.log("d", "Attempting to connect with [%s]" % key)
                    # It should already be set, but if it actually connects we know for sure it's supported!
                    active_machine.addConfiguredConnectionType(self._discovered_devices[key].connectionType.value)
                    self._discovered_devices[key].connect()
                    self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
                else:
                    self._onDeviceConnectionStateChanged(key)
            else:
                if self._discovered_devices[key].isConnected():
                    Logger.log("d", "Attempting to close connection with [%s]" % key)
                    self._discovered_devices[key].close()
                    self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)

    def _onDeviceConnectionStateChanged(self, key):
        if key not in self._discovered_devices:
            return
        if self._discovered_devices[key].isConnected():
            # Sometimes the status changes after changing the global container and maybe the device doesn't belong to this machine
            um_network_key = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("um_network_key")
            if key == um_network_key:
                self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
                self.checkCloudFlowIsPossible()
        else:
            self.getOutputDeviceManager().removeOutputDevice(key)

    def stop(self):
        if self._zero_conf is not None:
            Logger.log("d", "zeroconf close...")
            self._zero_conf.close()
        self._cloud_output_device_manager.stop()

    def removeManualDevice(self, key, address = None):
        if key in self._discovered_devices:
            if not address:
                address = self._discovered_devices[key].ipAddress
            self._onRemoveDevice(key)
            self.resetLastManualDevice()

        if address in self._manual_instances:
            self._manual_instances.remove(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

    def addManualDevice(self, address):
        if address not in self._manual_instances:
            self._manual_instances.append(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

        instance_name = "manual:%s" % address
        properties = {
            b"name": address.encode("utf-8"),
            b"address": address.encode("utf-8"),
            b"manual": b"true",
            b"incomplete": b"true",
            b"temporary": b"true"   # Still a temporary device until all the info is retrieved in _onNetworkRequestFinished
        }

        if instance_name not in self._discovered_devices:
            # Add a preliminary printer instance
            self._onAddDevice(instance_name, address, properties)
        self._last_manual_entry_key = instance_name

        self._checkManualDevice(address)

    def _checkManualDevice(self, address):
        # Check if a UM3 family device exists at this address.
        # If a printer responds, it will replace the preliminary printer created above
        # origin=manual is for tracking back the origin of the call
        url = QUrl("http://" + address + self._api_prefix + "system")
        name_request = QNetworkRequest(url)
        self._network_manager.get(name_request)

    def _onNetworkRequestFinished(self, reply):
        reply_url = reply.url().toString()

        if "system" in reply_url:
            if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
                # Something went wrong with checking the firmware version!
                return

            try:
                system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return

            address = reply.url().host()
            has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version
            instance_name = "manual:%s" % address
            properties = {
                b"name": (system_info["name"] + " (manual)").encode("utf-8"),
                b"address": address.encode("utf-8"),
                b"firmware_version": system_info["firmware"].encode("utf-8"),
                b"manual": b"true",
                b"machine": str(system_info['hardware']["typeid"]).encode("utf-8")
            }

            if has_cluster_capable_firmware:
                # Cluster needs an additional request, before it's completed.
                properties[b"incomplete"] = b"true"

            # Check if the device is still in the list & re-add it with the updated
            # information.
            if instance_name in self._discovered_devices:
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

            if has_cluster_capable_firmware:
                # We need to request more info in order to figure out the size of the cluster.
                cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/")
                cluster_request = QNetworkRequest(cluster_url)
                self._network_manager.get(cluster_request)

        elif "printers" in reply_url:
            if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
                # Something went wrong with checking the amount of printers the cluster has!
                return
            # So we confirmed that the device is in fact a cluster printer, and we should now know how big it is.
            try:
                cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return
            address = reply.url().host()
            instance_name = "manual:%s" % address
            if instance_name in self._discovered_devices:
                device = self._discovered_devices[instance_name]
                properties = device.getProperties().copy()
                if b"incomplete" in properties:
                    del properties[b"incomplete"]
                properties[b"cluster_size"] = len(cluster_printers_list)
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

    def _onRemoveDevice(self, device_id):
        device = self._discovered_devices.pop(device_id, None)
        if device:
            if device.isConnected():
                device.disconnect()
                try:
                    device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
                except TypeError:
                    # Disconnect already happened.
                    pass

            self.discoveredDevicesChanged.emit()

    def _onAddDevice(self, name, address, properties):
        # Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster"
        # or "Legacy" UM3 device.
        cluster_size = int(properties.get(b"cluster_size", -1))

        printer_type = properties.get(b"machine", b"").decode("utf-8")
        printer_type_identifiers = {
            "9066": "ultimaker3",
            "9511": "ultimaker3_extended",
            "9051": "ultimaker_s5"
        }

        for key, value in printer_type_identifiers.items():
            if printer_type.startswith(key):
                properties[b"printer_type"] = bytes(value, encoding="utf8")
                break
        else:
            properties[b"printer_type"] = b"Unknown"
        if cluster_size >= 0:
            device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties)
        else:
            device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties)

        self._discovered_devices[device.getId()] = device
        self.discoveredDevicesChanged.emit()

        global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
        if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"):
            # Ensure that the configured connection type is set.
            global_container_stack.addConfiguredConnectionType(device.connectionType.value)
            device.connect()
            device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged)

    ##  Appends a service changed request so later the handling thread will pick it up and processes it.
    def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
        # append the request and set the event so the event handling thread can pick it up
        item = (zeroconf, service_type, name, state_change)
        self._service_changed_request_queue.put(item)
        self._service_changed_request_event.set()

    def _handleOnServiceChangedRequests(self):
        while True:
            # Wait for the event to be set
            self._service_changed_request_event.wait(timeout = 5.0)

            # Stop if the application is shutting down
            if CuraApplication.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)

    ##  Handler for zeroConf detection.
    #   Return True or False indicating if the process succeeded.
    #   Note that this function can take over 3 seconds to complete. Be careful
    #   calling it from the main thread.
    def _onServiceChanged(self, zero_conf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            # First try getting info from zero-conf cache
            info = ServiceInfo(service_type, name, properties = {})
            for record in zero_conf.cache.entries_with_name(name.lower()):
                info.update_record(zero_conf, time(), record)

            for record in zero_conf.cache.entries_with_name(info.server):
                info.update_record(zero_conf, time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                info = zero_conf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type", None)
                if type_of_device:
                    if type_of_device == b"printer":
                        address = '.'.join(map(lambda n: str(n), info.address))
                        self.addDeviceSignal.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.removeDeviceSignal.emit(str(name))

        return True

    ## Check if the prerequsites are in place to start the cloud flow
    def checkCloudFlowIsPossible(self) -> None:
        Logger.log("d", "Checking if cloud connection is possible...")

        # Pre-Check: Skip if active machine already has been cloud connected or you said don't ask again
        active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"]
        if active_machine:
            
            # Check 1A: Printer isn't already configured for cloud
            if ConnectionType.CloudConnection.value in active_machine.configuredConnectionTypes:
                Logger.log("d", "Active machine was already configured for cloud.")
                return
            
            # Check 1B: Printer isn't already configured for cloud
            if active_machine.getMetaDataEntry("cloud_flow_complete", False):
                Logger.log("d", "Active machine was already configured for cloud.")
                return

            # Check 2: User did not already say "Don't ask me again"
            if active_machine.getMetaDataEntry("do_not_show_cloud_message", False):
                Logger.log("d", "Active machine shouldn't ask about cloud anymore.")
                return
        
            # Check 3: User is logged in with an Ultimaker account
            if not self._account.isLoggedIn:
                Logger.log("d", "Cloud Flow not possible: User not logged in!")
                return

            # Check 4: Machine is configured for network connectivity
            if not self._application.getMachineManager().activeMachineHasNetworkConnection:
                Logger.log("d", "Cloud Flow not possible: Machine is not connected!")
                return
            
            # Check 5: Machine has correct firmware version
            firmware_version = self._application.getMachineManager().activeMachineFirmwareVersion # type: str
            if not Version(firmware_version) > self._min_cloud_version:
                Logger.log("d", "Cloud Flow not possible: Machine firmware (%s) is too low! (Requires version %s)",
                                firmware_version,
                                self._min_cloud_version)
                return
            
            Logger.log("d", "Cloud flow is possible!")
            self.cloudFlowIsPossible.emit()

    def _onCloudFlowPossible(self) -> None:
        # Cloud flow is possible, so show the message
        if not self._start_cloud_flow_message:
            self._createCloudFlowStartMessage()
        if self._start_cloud_flow_message and not self._start_cloud_flow_message.visible:
            self._start_cloud_flow_message.show()        

    def _onCloudPrintingConfigured(self) -> None:
        # Hide the cloud flow start message if it was hanging around already
        # For example: if the user already had the browser openen and made the association themselves
        if self._start_cloud_flow_message and self._start_cloud_flow_message.visible:
            self._start_cloud_flow_message.hide()
        
        # Cloud flow is complete, so show the message
        if not self._cloud_flow_complete_message:
            self._createCloudFlowCompleteMessage()
        if self._cloud_flow_complete_message and not self._cloud_flow_complete_message.visible:
            self._cloud_flow_complete_message.show()
        
        # Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers
        active_machine = self._application.getMachineManager().activeMachine
        if active_machine:
            active_machine.setMetaDataEntry("do_not_show_cloud_message", True)
        return

    def _onDontAskMeAgain(self, checked: bool) -> None:
        active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"]
        if active_machine:
            active_machine.setMetaDataEntry("do_not_show_cloud_message", checked)
            if checked:
                Logger.log("d", "Will not ask the user again to cloud connect for current printer.")
        return

    def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None:
        address = self._application.getMachineManager().activeMachineAddress # type: str
        if address:
            QDesktopServices.openUrl(QUrl("http://" + address + "/cloud_connect"))
            if self._start_cloud_flow_message:
                self._start_cloud_flow_message.hide()
                self._start_cloud_flow_message = None
        return
    
    def _onReviewCloudConnection(self, messageId: str, actionId: str) -> None:
        address = self._application.getMachineManager().activeMachineAddress # type: str
        if address:
            QDesktopServices.openUrl(QUrl("http://" + address + "/settings"))
        return

    def _onMachineSwitched(self) -> None:
        # Hide any left over messages
        if self._start_cloud_flow_message is not None and self._start_cloud_flow_message.visible:
            self._start_cloud_flow_message.hide()
        if self._cloud_flow_complete_message is not None and self._cloud_flow_complete_message.visible:
            self._cloud_flow_complete_message.hide()

        # Check for cloud flow again with newly selected machine
        self.checkCloudFlowIsPossible()

    def _createCloudFlowStartMessage(self):
        self._start_cloud_flow_message = Message(
            text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."),
            lifetime = 0,
            image_source = QUrl.fromLocalFile(os.path.join(
                PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
                "resources", "svg", "cloud-flow-start.svg"
            )),
            image_caption = i18n_catalog.i18nc("@info:status Ultimaker Cloud is a brand name and shouldn't be translated.", "Connect to Ultimaker Cloud"),
            option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."),
            option_state = False
        )
        self._start_cloud_flow_message.addAction("", i18n_catalog.i18nc("@action", "Get started"), "", "")
        self._start_cloud_flow_message.optionToggled.connect(self._onDontAskMeAgain)
        self._start_cloud_flow_message.actionTriggered.connect(self._onCloudFlowStarted)

    def _createCloudFlowCompleteMessage(self):
        self._cloud_flow_complete_message = Message(
            text = i18n_catalog.i18nc("@info:status", "You can now send and monitor print jobs from anywhere using your Ultimaker account."),
            lifetime = 30,
            image_source = QUrl.fromLocalFile(os.path.join(
                PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
                "resources", "svg", "cloud-flow-completed.svg"
            )),
            image_caption = i18n_catalog.i18nc("@info:status", "Connected!")
        )
        self._cloud_flow_complete_message.addAction("", i18n_catalog.i18nc("@action", "Review your connection"), "", "", 1) # TODO: Icon
        self._cloud_flow_complete_message.actionTriggered.connect(self._onReviewCloudConnection)
예제 #51
0
파일: Scene.py 프로젝트: Ultimaker/Uranium
class Scene:
    def __init__(self) -> None:
        super().__init__()

        from UM.Scene.SceneNode import SceneNode
        self._root = SceneNode(name = "Root")
        self._root.setCalculateBoundingBox(False)
        self._connectSignalsRoot()
        self._active_camera = None  # type: Optional[Camera]
        self._ignore_scene_changes = False
        self._lock = threading.Lock()

        # Watching file for changes.
        self._file_watcher = QFileSystemWatcher()
        self._file_watcher.fileChanged.connect(self._onFileChanged)

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

    def _connectSignalsRoot(self) -> None:
        self._root.transformationChanged.connect(self.sceneChanged)
        self._root.childrenChanged.connect(self.sceneChanged)
        self._root.meshDataChanged.connect(self.sceneChanged)

    def _disconnectSignalsRoot(self) -> None:
        self._root.transformationChanged.disconnect(self.sceneChanged)
        self._root.childrenChanged.disconnect(self.sceneChanged)
        self._root.meshDataChanged.disconnect(self.sceneChanged)

    def setIgnoreSceneChanges(self, ignore_scene_changes: bool) -> None:
        if self._ignore_scene_changes != ignore_scene_changes:
            self._ignore_scene_changes = ignore_scene_changes
            if self._ignore_scene_changes:
                self._disconnectSignalsRoot()
            else:
                self._connectSignalsRoot()

    ##  Gets the global scene lock.
    #
    #   Use this lock to prevent any read or write actions on the scene from other threads,
    #   assuming those threads also properly acquire the lock. Most notably, this
    #   prevents the rendering thread from rendering the scene while it is changing.
    def getSceneLock(self) -> threading.Lock:
        return self._lock

    ##  Get the root node of the scene.
    def getRoot(self) -> "SceneNode":
        return self._root

    ##  Change the root node of the scene
    def setRoot(self, node: "SceneNode") -> None:
        if self._root != node:
            if not self._ignore_scene_changes:
                self._disconnectSignalsRoot()
            self._root = node
            if not self._ignore_scene_changes:
                self._connectSignalsRoot()
            self.rootChanged.emit()

    rootChanged = Signal()

    ##  Get the camera that should be used for rendering.
    def getActiveCamera(self) -> Optional[Camera]:
        return self._active_camera

    def getAllCameras(self) -> List[Camera]:
        cameras = []
        for node in BreadthFirstIterator(self._root):  # type: ignore
            if isinstance(node, Camera):
                cameras.append(node)
        return cameras

    ##  Set the camera that should be used for rendering.
    #   \param name The name of the camera to use.
    def setActiveCamera(self, name: str) -> None:
        camera = self.findCamera(name)
        if camera:
            self._active_camera = camera
        else:
            Logger.log("w", "Couldn't find camera with name [%s] to activate!" % name)

    ##  Signal that is emitted whenever something in the scene changes.
    #   \param object The object that triggered the change.
    sceneChanged = Signal()

    ##  Find an object by id.
    #
    #   \param object_id The id of the object to search for, as returned by the python id() method.
    #
    #   \return The object if found, or None if not.
    def findObject(self, object_id: int) -> Optional["SceneNode"]:
        for node in BreadthFirstIterator(self._root):  # type: ignore
            if id(node) == object_id:
                return node
        return None

    def findCamera(self, name: str) -> Optional[Camera]:
        for node in BreadthFirstIterator(self._root):  # type: ignore
            if isinstance(node, Camera) and node.getName() == name:
                return node
        return None

    ##  Add a file to be watched for changes.
    #   \param file_path The path to the file that must be watched.
    def addWatchedFile(self, file_path: str) -> None:
        # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive
        if not Platform.isWindows():
            self._file_watcher.addPath(file_path)

    ##  Remove a file so that it will no longer be watched for changes.
    #   \param file_path The path to the file that must no longer be watched.
    def removeWatchedFile(self, file_path: str) -> None:
        # The QT 5.10.0 issue, only on Windows. Cura crashes after loading a stl file from USB/sd-card/Cloud-based drive
        if not Platform.isWindows():
            self._file_watcher.removePath(file_path)

    ##  Triggered whenever a file is changed that we currently have loaded.
    def _onFileChanged(self, file_path: str) -> None:
        if not os.path.isfile(file_path) or os.path.getsize(file_path) == 0:  # File doesn't exist any more, or it is empty
            return

        # Multiple nodes may be loaded from the same file at different stages. Reload them all.
        from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator  # To find which nodes to reload when files have changed.
        modified_nodes = [node for node in DepthFirstIterator(self.getRoot()) if node.getMeshData() and node.getMeshData().getFileName() == file_path]  # type: ignore

        if modified_nodes:
            # Hide the message if it was already visible
            if self._reload_message is not None:
                self._reload_message.hide()

            self._reload_message = Message(i18n_catalog.i18nc("@info", "Would you like to reload {filename}?").format(filename = os.path.basename(file_path)),
                              title = i18n_catalog.i18nc("@info:title", "File has been modified"))
            self._reload_message.addAction("reload", i18n_catalog.i18nc("@action:button", "Reload"), icon = "", description = i18n_catalog.i18nc("@action:description", "This will trigger the modified files to reload from disk."))
            self._reload_callback = functools.partial(self._reloadNodes, modified_nodes)
            self._reload_message.actionTriggered.connect(self._reload_callback)
            self._reload_message.show()

    ##  Reloads a list of nodes after the user pressed the "Reload" button.
    #   \param nodes The list of nodes that needs to be reloaded.
    #   \param message The message that triggered the action to reload them.
    #   \param action The button that triggered the action to reload them.
    def _reloadNodes(self, nodes: List["SceneNode"], message: str, action: str) -> None:
        if action != "reload":
            return
        if self._reload_message is not None:
            self._reload_message.hide()
        for node in nodes:
            meshdata = node.getMeshData()
            if meshdata:
                filename = meshdata.getFileName()
                if not filename or not os.path.isfile(filename):  # File doesn't exist any more.
                    continue
                job = ReadMeshJob(filename)
                self._reload_finished_callback = functools.partial(self._reloadJobFinished, node)
                job.finished.connect(self._reload_finished_callback)
                job.start()

    ##  Triggered when reloading has finished.
    #
    #   This then puts the resulting mesh data in the node.
    def _reloadJobFinished(self, replaced_node: SceneNode, job: ReadMeshJob) -> None:
        for node in job.getResult():
            mesh_data = node.getMeshData()
            if mesh_data:
                replaced_node.setMeshData(mesh_data)
            else:
                Logger.log("w", "Could not find a mesh in reloaded node.")
예제 #52
0
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
    def __init__(self, device_id, address: str, properties, parent = None) -> None:
        super().__init__(device_id = device_id, address = address, properties = properties, connection_type =  ConnectionType.NetworkConnection, parent = parent)
        self._api_prefix = "/api/v1/"
        self._number_of_extruders = 2

        self._authentication_id = None
        self._authentication_key = None

        self._authentication_counter = 0
        self._max_authentication_counter = 5 * 60  # Number of attempts before authentication timed out (5 min)

        self._authentication_timer = QTimer()
        self._authentication_timer.setInterval(1000)  # TODO; Add preference for update interval
        self._authentication_timer.setSingleShot(False)

        self._authentication_timer.timeout.connect(self._onAuthenticationTimer)

        # The messages are created when connect is called the first time.
        # This ensures that the messages are only created for devices that actually want to connect.
        self._authentication_requested_message = None
        self._authentication_failed_message = None
        self._authentication_succeeded_message = None
        self._not_authenticated_message = None

        self.authenticationStateChanged.connect(self._onAuthenticationStateChanged)

        self.setPriority(3)  # Make sure the output device gets selected above local file output
        self.setName(self._id)
        self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
        self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))

        self.setIconName("print")

        self._output_controller = LegacyUM3PrinterOutputController(self)

    def _createMonitorViewFromQML(self) -> None:
        if self._monitor_view_qml_path is None and PluginRegistry.getInstance() is not None:
            self._monitor_view_qml_path = os.path.join(
                PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
                "resources", "qml", "MonitorStage.qml"
            )
        super()._createMonitorViewFromQML()

    def _onAuthenticationStateChanged(self):
        # We only accept commands if we are authenticated.
        self._setAcceptsCommands(self._authentication_state == AuthState.Authenticated)

        if self._authentication_state == AuthState.Authenticated:
            self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network."))
        elif self._authentication_state == AuthState.AuthenticationRequested:
            self.setConnectionText(i18n_catalog.i18nc("@info:status",
                                                      "Connected over the network. Please approve the access request on the printer."))
        elif self._authentication_state == AuthState.AuthenticationDenied:
            self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. No access to control the printer."))


    def _setupMessages(self):
        self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status",
                                                                            "Access to the printer requested. Please approve the request on the printer"),
                                                         lifetime=0, dismissable=False, progress=0,
                                                         title=i18n_catalog.i18nc("@info:title",
                                                                                  "Authentication status"))

        self._authentication_failed_message = Message("", title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
        self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None,
                                                      i18n_catalog.i18nc("@info:tooltip", "Re-send the access request"))
        self._authentication_failed_message.actionTriggered.connect(self._messageCallback)
        self._authentication_succeeded_message = Message(
            i18n_catalog.i18nc("@info:status", "Access to the printer accepted"),
            title=i18n_catalog.i18nc("@info:title", "Authentication Status"))

        self._not_authenticated_message = Message(
            i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."),
            title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
        self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"),
                                                  None, i18n_catalog.i18nc("@info:tooltip",
                                                                           "Send access request to the printer"))
        self._not_authenticated_message.actionTriggered.connect(self._messageCallback)

    def _messageCallback(self, message_id=None, action_id="Retry"):
        if action_id == "Request" or action_id == "Retry":
            if self._authentication_failed_message:
                self._authentication_failed_message.hide()
            if self._not_authenticated_message:
                self._not_authenticated_message.hide()

            self._requestAuthentication()

    def connect(self):
        super().connect()
        self._setupMessages()
        global_container = CuraApplication.getInstance().getGlobalContainerStack()
        if global_container:
            self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None)
            self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None)

    def close(self):
        super().close()
        if self._authentication_requested_message:
            self._authentication_requested_message.hide()
        if self._authentication_failed_message:
            self._authentication_failed_message.hide()
        if self._authentication_succeeded_message:
            self._authentication_succeeded_message.hide()
        self._sending_gcode = False
        self._compressing_gcode = False
        self._authentication_timer.stop()

    ##  Send all material profiles to the printer.
    def _sendMaterialProfiles(self):
        Logger.log("i", "Sending material profiles to printer")

        # TODO: Might want to move this to a job...
        for container in ContainerRegistry.getInstance().findInstanceContainers(type="material"):
            try:
                xml_data = container.serialize()
                if xml_data == "" or xml_data is None:
                    continue

                names = ContainerManager.getInstance().getLinkedMaterials(container.getId())
                if names:
                    # There are other materials that share this GUID.
                    if not container.isReadOnly():
                        continue  # If it's not readonly, it's created by user, so skip it.

                file_name = "none.xml"

                self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), on_finished=None)

            except NotImplementedError:
                # If the material container is not the most "generic" one it can't be serialized an will raise a
                # NotImplementedError. We can simply ignore these.
                pass

    def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
        if not self.activePrinter:
            # No active printer. Unable to write
            return

        if self.activePrinter.state not in ["idle", ""]:
            # Printer is not able to accept commands.
            return

        if self._authentication_state != AuthState.Authenticated:
            # Not authenticated, so unable to send job.
            return

        self.writeStarted.emit(self)

        gcode_dict = getattr(CuraApplication.getInstance().getController().getScene(), "gcode_dict", [])
        active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
        gcode_list = gcode_dict[active_build_plate_id]

        if not gcode_list:
            # Unable to find g-code. Nothing to send
            return

        self._gcode = gcode_list

        errors = self._checkForErrors()
        if errors:
            text = i18n_catalog.i18nc("@label", "Unable to start a new print job.")
            informative_text = i18n_catalog.i18nc("@label",
                                                  "There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. "
                                                  "Please resolve this issues before continuing.")
            detailed_text = ""
            for error in errors:
                detailed_text += error + "\n"

            CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
                                                 text,
                                                 informative_text,
                                                 detailed_text,
                                                 buttons=QMessageBox.Ok,
                                                 icon=QMessageBox.Critical,
                                                callback = self._messageBoxCallback
                                                 )
            return  # Don't continue; Errors must block sending the job to the printer.

        # There might be multiple things wrong with the configuration. Check these before starting.
        warnings = self._checkForWarnings()

        if warnings:
            text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?")
            informative_text = i18n_catalog.i18nc("@label",
                                                  "There is a mismatch between the configuration or calibration of the printer and Cura. "
                                                  "For the best result, always slice for the PrintCores and materials that are inserted in your printer.")
            detailed_text = ""
            for warning in warnings:
                detailed_text += warning + "\n"

            CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
                                                 text,
                                                 informative_text,
                                                 detailed_text,
                                                 buttons=QMessageBox.Yes + QMessageBox.No,
                                                 icon=QMessageBox.Question,
                                                 callback=self._messageBoxCallback
                                                 )
            return

        # No warnings or errors, so we're good to go.
        self._startPrint()

        # Notify the UI that a switch to the print monitor should happen
        CuraApplication.getInstance().getController().setActiveStage("MonitorStage")

    def _startPrint(self):
        Logger.log("i", "Sending print job to printer.")
        if self._sending_gcode:
            self._error_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Sending new jobs (temporarily) blocked, still sending the previous print job."))
            self._error_message.show()
            return

        self._sending_gcode = True

        self._send_gcode_start = time()
        self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
                                         i18n_catalog.i18nc("@info:title", "Sending Data"))
        self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
        self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
        self._progress_message.show()
        
        compressed_gcode = self._compressGCode()
        if compressed_gcode is None:
            # Abort was called.
            return

        file_name = "%s.gcode.gz" % CuraApplication.getInstance().getPrintInformation().jobName
        self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode,
                      on_finished=self._onPostPrintJobFinished)

        return

    def _progressMessageActionTriggered(self, message_id=None, action_id=None):
        if action_id == "Abort":
            Logger.log("d", "User aborted sending print to remote.")
            self._progress_message.hide()
            self._compressing_gcode = False
            self._sending_gcode = False
            CuraApplication.getInstance().getController().setActiveStage("PrepareStage")

    def _onPostPrintJobFinished(self, reply):
        self._progress_message.hide()
        self._sending_gcode = False

    def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()
            if new_progress > self._progress_message.getProgress():
                self._progress_message.show()  # Ensure that the message is visible.
                self._progress_message.setProgress(bytes_sent / bytes_total * 100)
        else:
            self._progress_message.setProgress(0)

            self._progress_message.hide()

    def _messageBoxCallback(self, button):
        def delayedCallback():
            if button == QMessageBox.Yes:
                self._startPrint()
            else:
                CuraApplication.getInstance().getController().setActiveStage("PrepareStage")
                # For some unknown reason Cura on OSX will hang if we do the call back code
                # immediately without first returning and leaving QML's event system.

        QTimer.singleShot(100, delayedCallback)

    def _checkForErrors(self):
        errors = []
        print_information = CuraApplication.getInstance().getPrintInformation()
        if not print_information.materialLengths:
            Logger.log("w", "There is no material length information. Unable to check for errors.")
            return errors

        for index, extruder in enumerate(self.activePrinter.extruders):
            # Due to airflow issues, both slots must be loaded, regardless if they are actually used or not.
            if extruder.hotendID == "":
                # No Printcore loaded.
                errors.append(i18n_catalog.i18nc("@info:status", "No Printcore loaded in slot {slot_number}".format(slot_number=index + 1)))

            if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
                # The extruder is by this print.
                if extruder.activeMaterial is None:
                    # No active material
                    errors.append(i18n_catalog.i18nc("@info:status", "No material loaded in slot {slot_number}".format(slot_number=index + 1)))
        return errors

    def _checkForWarnings(self):
        warnings = []
        print_information = CuraApplication.getInstance().getPrintInformation()

        if not print_information.materialLengths:
            Logger.log("w", "There is no material length information. Unable to check for warnings.")
            return warnings

        extruder_manager = ExtruderManager.getInstance()

        for index, extruder in enumerate(self.activePrinter.extruders):
            if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
                # The extruder is by this print.

                # TODO: material length check

                # Check if the right Printcore is active.
                variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"})
                if variant:
                    if variant.getName() != extruder.hotendID:
                        warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}".format(cura_printcore_name = variant.getName(), remote_printcore_name = extruder.hotendID, extruder_id = index + 1)))
                else:
                    Logger.log("w", "Unable to find variant.")

                # Check if the right material is loaded.
                local_material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"})
                if local_material:
                    if extruder.activeMaterial.guid != local_material.getMetaDataEntry("GUID"):
                        Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID"))
                        warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(local_material.getName(), extruder.activeMaterial.name, index + 1))
                else:
                    Logger.log("w", "Unable to find material.")

        return warnings

    def _update(self):
        if not super()._update():
            return
        if self._authentication_state == AuthState.NotAuthenticated:
            if self._authentication_id is None and self._authentication_key is None:
                # This machine doesn't have any authentication, so request it.
                self._requestAuthentication()
            elif self._authentication_id is not None and self._authentication_key is not None:
                # We have authentication info, but we haven't checked it out yet. Do so now.
                self._verifyAuthentication()
        elif self._authentication_state == AuthState.AuthenticationReceived:
            # We have an authentication, but it's not confirmed yet.
            self._checkAuthentication()

        # We don't need authentication for requesting info, so we can go right ahead with requesting this.
        self.get("printer", on_finished=self._onGetPrinterDataFinished)
        self.get("print_job", on_finished=self._onGetPrintJobFinished)

    def _resetAuthenticationRequestedMessage(self):
        if self._authentication_requested_message:
            self._authentication_requested_message.hide()
        self._authentication_timer.stop()
        self._authentication_counter = 0

    def _onAuthenticationTimer(self):
        self._authentication_counter += 1
        self._authentication_requested_message.setProgress(
            self._authentication_counter / self._max_authentication_counter * 100)
        if self._authentication_counter > self._max_authentication_counter:
            self._authentication_timer.stop()
            Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id)
            self.setAuthenticationState(AuthState.AuthenticationDenied)
            self._resetAuthenticationRequestedMessage()
            self._authentication_failed_message.show()

    def _verifyAuthentication(self):
        Logger.log("d", "Attempting to verify authentication")
        # This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator.
        self.get("auth/verify", on_finished=self._onVerifyAuthenticationCompleted)

    def _onVerifyAuthenticationCompleted(self, reply):
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if status_code == 401:
            # Something went wrong; We somehow tried to verify authentication without having one.
            Logger.log("d", "Attempted to verify auth without having one.")
            self._authentication_id = None
            self._authentication_key = None
            self.setAuthenticationState(AuthState.NotAuthenticated)
        elif status_code == 403 and self._authentication_state != AuthState.Authenticated:
            # If we were already authenticated, we probably got an older message back all of the sudden. Drop that.
            Logger.log("d",
                       "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ",
                       self._authentication_state)
            self.setAuthenticationState(AuthState.AuthenticationDenied)
            self._authentication_failed_message.show()
        elif status_code == 200:
            self.setAuthenticationState(AuthState.Authenticated)

    def _checkAuthentication(self):
        Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
        self.get("auth/check/" + str(self._authentication_id), on_finished=self._onCheckAuthenticationFinished)

    def _onCheckAuthenticationFinished(self, reply):
        if str(self._authentication_id) not in reply.url().toString():
            Logger.log("w", "Got an old id response.")
            # Got response for old authentication ID.
            return
        try:
            data = json.loads(bytes(reply.readAll()).decode("utf-8"))
        except json.decoder.JSONDecodeError:
            Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.")
            return

        if data.get("message", "") == "authorized":
            Logger.log("i", "Authentication was approved")
            self.setAuthenticationState(AuthState.Authenticated)
            self._saveAuthentication()

            # Double check that everything went well.
            self._verifyAuthentication()

            # Notify the user.
            self._resetAuthenticationRequestedMessage()
            self._authentication_succeeded_message.show()
        elif data.get("message", "") == "unauthorized":
            Logger.log("i", "Authentication was denied.")
            self.setAuthenticationState(AuthState.AuthenticationDenied)
            self._authentication_failed_message.show()

    def _saveAuthentication(self) -> None:
        global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
        if self._authentication_key is None:
            Logger.log("e", "Authentication key is None, nothing to save.")
            return
        if self._authentication_id is None:
            Logger.log("e", "Authentication id is None, nothing to save.")
            return
        if global_container_stack:
            global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key)

            global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id)

            # Force save so we are sure the data is not lost.
            CuraApplication.getInstance().saveStack(global_container_stack)
            Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id,
                       self._getSafeAuthKey())
        else:
            Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id,
                       self._getSafeAuthKey())

    def _onRequestAuthenticationFinished(self, reply):
        try:
            data = json.loads(bytes(reply.readAll()).decode("utf-8"))
        except json.decoder.JSONDecodeError:
            Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.")
            self.setAuthenticationState(AuthState.NotAuthenticated)
            return

        self.setAuthenticationState(AuthState.AuthenticationReceived)
        self._authentication_id = data["id"]
        self._authentication_key = data["key"]
        Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.",
                   self._authentication_id, self._getSafeAuthKey())

    def _requestAuthentication(self):
        self._authentication_requested_message.show()
        self._authentication_timer.start()

        # Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might
        # give issues.
        self._authentication_key = None
        self._authentication_id = None

        self.post("auth/request",
                  json.dumps({"application": "Cura-" + CuraApplication.getInstance().getVersion(),
                              "user": self._getUserName()}),
                  on_finished=self._onRequestAuthenticationFinished)

        self.setAuthenticationState(AuthState.AuthenticationRequested)

    def _onAuthenticationRequired(self, reply, authenticator):
        if self._authentication_id is not None and self._authentication_key is not None:
            Logger.log("d",
                       "Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s",
                       self._id, self._authentication_id, self._getSafeAuthKey())
            authenticator.setUser(self._authentication_id)
            authenticator.setPassword(self._authentication_key)
        else:
            Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._id)

    def _onGetPrintJobFinished(self, reply):
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)

        if not self._printers:
            return  # Ignore the data for now, we don't have info about a printer yet.
        printer = self._printers[0]

        if status_code == 200:
            try:
                result = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except json.decoder.JSONDecodeError:
                Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
                return
            if printer.activePrintJob is None:
                print_job = PrintJobOutputModel(output_controller=self._output_controller)
                printer.updateActivePrintJob(print_job)
            else:
                print_job = printer.activePrintJob
            print_job.updateState(result["state"])
            print_job.updateTimeElapsed(result["time_elapsed"])
            print_job.updateTimeTotal(result["time_total"])
            print_job.updateName(result["name"])
        elif status_code == 404:
            # No job found, so delete the active print job (if any!)
            printer.updateActivePrintJob(None)
        else:
            Logger.log("w",
                       "Got status code {status_code} while trying to get printer data".format(status_code=status_code))

    def materialHotendChangedMessage(self, callback):
        CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"),
                                             i18n_catalog.i18nc("@label",
                                                                "Would you like to use your current printer configuration in Cura?"),
                                             i18n_catalog.i18nc("@label",
                                                                "The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."),
                                             buttons=QMessageBox.Yes + QMessageBox.No,
                                             icon=QMessageBox.Question,
                                             callback=callback
                                             )

    def _onGetPrinterDataFinished(self, reply):
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if status_code == 200:
            try:
                result = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except json.decoder.JSONDecodeError:
                Logger.log("w", "Received an invalid printer state message: Not valid JSON.")
                return

            if not self._printers:
                # Quickest way to get the firmware version is to grab it from the zeroconf.
                firmware_version = self._properties.get(b"firmware_version", b"").decode("utf-8")
                self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version)]
                self._printers[0].setCameraUrl(QUrl("http://" + self._address + ":8080/?action=stream"))
                for extruder in self._printers[0].extruders:
                    extruder.activeMaterialChanged.connect(self.materialIdChanged)
                    extruder.hotendIDChanged.connect(self.hotendIdChanged)
                self.printersChanged.emit()

            # LegacyUM3 always has a single printer.
            printer = self._printers[0]
            printer.updateBedTemperature(result["bed"]["temperature"]["current"])
            printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"])
            printer.updateState(result["status"])

            try:
                # If we're still handling the request, we should ignore remote for a bit.
                if not printer.getController().isPreheatRequestInProgress():
                    printer.updateIsPreheating(result["bed"]["pre_heat"]["active"])
            except KeyError:
                # Older firmwares don't support preheating, so we need to fake it.
                pass

            head_position = result["heads"][0]["position"]
            printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"])

            for index in range(0, self._number_of_extruders):
                temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"]
                extruder = printer.extruders[index]
                extruder.updateTargetHotendTemperature(temperatures["target"])
                extruder.updateHotendTemperature(temperatures["current"])

                material_guid = result["heads"][0]["extruders"][index]["active_material"]["guid"]

                if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid:
                    # Find matching material (as we need to set brand, type & color)
                    containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
                                                                                        GUID=material_guid)
                    if containers:
                        color = containers[0].getMetaDataEntry("color_code")
                        brand = containers[0].getMetaDataEntry("brand")
                        material_type = containers[0].getMetaDataEntry("material")
                        name = containers[0].getName()
                    else:
                        # Unknown material.
                        color = "#00000000"
                        brand = "Unknown"
                        material_type = "Unknown"
                        name = "Unknown"
                    material = MaterialOutputModel(guid=material_guid, type=material_type,
                                                   brand=brand, color=color, name = name)
                    extruder.updateActiveMaterial(material)

                try:
                    hotend_id = result["heads"][0]["extruders"][index]["hotend"]["id"]
                except KeyError:
                    hotend_id = ""
                printer.extruders[index].updateHotendID(hotend_id)

        else:
            Logger.log("w",
                       "Got status code {status_code} while trying to get printer data".format(status_code = status_code))

    ##  Convenience function to "blur" out all but the last 5 characters of the auth key.
    #   This can be used to debug print the key, without it compromising the security.
    def _getSafeAuthKey(self):
        if self._authentication_key is not None:
            result = self._authentication_key[-5:]
            result = "********" + result
            return result

        return self._authentication_key
예제 #53
0
파일: SliceInfo.py 프로젝트: Cycloneee/Cura
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 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):
        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()

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

            # TODO: Send material per extruder instead of mashing it on a pile
            material_used = math.pi * material_radius * material_radius * sum(print_information.materialLengths) #Volume of all materials 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(),
                "settings": global_container_stack.serialize(), # global_container with references on used containers
                "version": Application.getInstance().getVersion(),
                "modelhash": "None",
                "printtime": print_information.currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601),
                "filament": material_used,
                "language": Preferences.getInstance().getValue("general/language"),
            }
            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:
                    submitted_data["settings_%s" %(container_id)] = container_serialized # This can be anything, eg. INI, JSON, etc.
                else:
                    Logger.log("i", "No data found in %s to be serialized!", container_id)

            # 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.
예제 #54
0
def test_addAction():
    message = Message()
    message.addAction(action_id = "blarg", name = "zomg", icon = "NO ICON", description="SuperAwesomeMessage")

    assert len(message.getActions()) == 1